diff --git a/.github/workflows/flutter_ios.yaml b/.github/workflows/flutter_ios.yaml index bb6169ab2f..ca0af8e1c9 100644 --- a/.github/workflows/flutter_ios.yaml +++ b/.github/workflows/flutter_ios.yaml @@ -60,7 +60,7 @@ jobs: if: inputs.has-native-tests run: | set -o pipefail - XCODEBUILD_DESTINATION="platform=iOS Simulator,name=iPhone 14 Pro Max,OS=latest" + XCODEBUILD_DESTINATION="platform=iOS Simulator,name=iPhone 16 Pro Max,OS=latest" xcodebuild test \ -workspace Runner.xcworkspace \ -scheme Runner \ diff --git a/README.md b/README.md index cd1ae88ca2..389271909e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ We are iterating and looking for feedback and collaboration, so please [**let us ⚠️ **Amplify Flutter v1 is now in Maintenance Mode until April 30th, 2025. This means that we will continue to include updates to ensure compatibility with backend services and security. No new features will be introduced in v1.** -Please use the latest version (v2) of Amplify Flutter. If you are currently using v1, follow [these instructions](https://docs.amplify.aws/lib/project-setup/upgrade-guide/q/platform/flutter/) to upgrade to v2. +Please use the latest version (Gen 2) of Amplify Flutter. If you are currently using v1, follow [these instructions](https://docs.amplify.aws/lib/project-setup/upgrade-guide/q/platform/flutter/) to upgrade to v2. ## Supported Amplify Libraries @@ -56,9 +56,8 @@ We follow [semantic versioning for updating our dependencies](https://semver.org ## Documentation -- [Getting Started with Amplify Flutter](https://docs.amplify.aws/start/q/integration/flutter) +- [Getting Started with Amplify Flutter](https://docs.amplify.aws/flutter/start/quickstart/) - [Amplify Framework](https://docs.amplify.aws/) -- [Install the Amplify CLI](https://docs.amplify.aws/lib/project-setup/prereq/q/platform/flutter#install-and-configure-the-amplify-cli) - [Contributing to Amplify Flutter](CONTRIBUTING.md) ## Flutter Development Guide @@ -68,71 +67,31 @@ Amplify for Flutter is an open-source project and welcomes contributions from th #### Prerequisites - [Flutter](https://flutter.dev/docs/get-started/install) -- [Amplify CLI](https://docs.amplify.aws/lib/project-setup/prereq/q/platform/flutter#option-1-watch-the-video-guide) +- Node.js v18.17 or later +- npm v9 or later -#### Getting Started Amplify Flutter +#### Getting Started With Amplify Flutter 1. Open your Flutter project. If you do not have an active Flutter project, you can create one after installing the [Flutter development tooling](https://flutter.dev/docs/get-started/install) and running `flutter create ` in your terminal. -2. Using the Amplify CLI, run `amplify init` from the root of your project: +2. Run the following to create an amplify project: -If you have not configured the Amplify CLI, check out our documentation at [Amplify CLI Installation](https://docs.amplify.aws/lib/project-setup/prereq/q/platform/flutter#install-and-configure-the-amplify-cli). + `npm create amplify@latest -y` -```bash -==> amplify init -Note: It is recommended to run this command from the root of your app directory -? Enter a name for the project -The following configuration will be applied: +4. To use the Authenticator, you need to add the following dependencies to your project: -Project information -| Name: -| Environment: dev -| Default editor: Visual Studio Code -| App type: flutter -| Configuration file location: ./lib/ + ```yaml + dependencies: + amplify_auth_cognito: ^2.0.0 + amplify_authenticator: ^2.0.0 + amplify_flutter: ^2.0.0 + flutter: + sdk: flutter + ``` -? Initialize the project with the above configuration? Yes -Using default provider awscloudformation -? Select the authentication method you want to use: AWS profile +5. run `flutter pub get` -For more information on AWS Profiles, see: -https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html - -? Please choose the profile you want to use default -``` - -4. Add Amplify categories (choose defaults for this example): - - ```bash - $ amplify add auth # Choose default configuration after entering this command in your terminal. - ``` - -5. Push changes to the cloud to provision the backend resources: - - ```bash - $ amplify push - ``` - -6. In your pubspec.yaml file, add the following to `dependencies`: - -> Note: Do not include dependencies in your `pubspec` file that you are not using in your app. This can cause a configuration error in the underlying SDK. - -```yaml -dependencies: - amplify_auth_cognito: ^2.0.0 - amplify_authenticator: ^2.0.0 - amplify_flutter: ^2.0.0 - flutter: - sdk: flutter -``` - -7. From the terminal run - -```bash -flutter pub get -``` - -8. In your main.dart file, add: +6. Update your main.dart file to the following: ```dart import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; @@ -140,36 +99,30 @@ import 'package:amplify_authenticator/amplify_authenticator.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; import 'package:flutter/material.dart'; -import 'amplifyconfiguration.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatefulWidget { - const MyApp({super.key}); - - @override - State createState() => _MyAppState(); -} +import 'amplify_outputs.dart'; -class _MyAppState extends State { - @override - void initState() { - super.initState(); - _configureAmplify(); +Future main() async { + try { + WidgetsFlutterBinding.ensureInitialized(); + await _configureAmplify(); + runApp(const MyApp()); + } on AmplifyException catch (e) { + runApp(Text("Error configuring Amplify: ${e.message}")); } +} - Future _configureAmplify() async { - try { - await Amplify.addPlugin(AmplifyAuthCognito()); - await Amplify.configure(amplifyconfig); - safePrint('Successfully configured'); - } on Exception catch (e) { - safePrint('Error configuring Amplify: $e'); - } +Future _configureAmplify() async { + try { + await Amplify.addPlugin(AmplifyAuthCognito()); + await Amplify.configure(amplifyConfig); + safePrint('Successfully configured'); + } on Exception catch (e) { + safePrint('Error configuring Amplify: $e'); } +} +class MyApp extends StatelessWidget { + const MyApp({super.key}); @override Widget build(BuildContext context) { return Authenticator( @@ -177,7 +130,13 @@ class _MyAppState extends State { builder: Authenticator.builder(), home: const Scaffold( body: Center( - child: Text('You are logged in!'), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SignOutButton(), + Text('TODO Application'), + ], + ), ), ), ), @@ -186,17 +145,17 @@ class _MyAppState extends State { } ``` -9. Since Amplify Flutter supports 6 platforms with Flutter including iOS, Android, Web, and Desktop, some extra configuration may be required for each platform. Check out the [Platform Setup](https://docs.amplify.aws/lib/project-setup/platform-setup/q/platform/flutter/) guide to make sure you've completed the necessary steps. +7. Deploy your backend use Amplify's per-developer cloud sandbox. This feature provides a separate backend environment for every developer on a team, ideal for local development and testing. To run your application with a sandbox environment, you can run the following command: -10. Run `flutter run` to launch your app on the connected device. + `npx ampx sandbox --outputs-format dart --outputs-out-dir lib` -11. Once the app is loaded, tap on **Configure Amplify**, then on **Record Event** a few times. +8. Since Amplify Flutter supports 6 platforms with Flutter including iOS, Android, Web, and Desktop, some extra configuration may be required for each platform. Check out the [Platform Setup](https://docs.amplify.aws/flutter/start/platform-setup/) guide to make sure you've completed the necessary steps. -12. To see the events you recoded, run `amplify console analytics`. This will open the Amazon Pinpoint console for your project in your default web browser. Within about a minute you should start seeing the events populating in the Events section of then Pinpoint console. +9. Run `flutter run` to launch your app on the connected device. Congratulations, you've built your first Amplify app! 🎉 -For further documentation and Amplify Category usage, see the [documentation](https://docs.amplify.aws/lib/q/platform/flutter). +For further documentation and Amplify Category usage, see the [documentation](https://docs.amplify.aws/flutter/). --- diff --git a/infra-gen2/backends/storage/main/amplify/backend.ts b/infra-gen2/backends/storage/main/amplify/backend.ts index e9d462a0b3..c75ada8bba 100644 --- a/infra-gen2/backends/storage/main/amplify/backend.ts +++ b/infra-gen2/backends/storage/main/amplify/backend.ts @@ -1,26 +1,52 @@ import { defineBackend } from "@aws-amplify/backend"; import * as s3 from "aws-cdk-lib/aws-s3"; import { auth } from "./auth/resource"; -import { storage } from "./storage/resource"; +import { firstBucket, secondBucket } from "./storage/resource"; /** * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more */ const backend = defineBackend({ auth, - storage, + firstBucket, + secondBucket, }); // custom storage configurations -const s3Bucket = backend.storage.resources.bucket; +const s3Bucket = backend.firstBucket.resources.bucket; const cfnBucket = s3Bucket.node.defaultChild as s3.CfnBucket; +const s3SecondaryBucket = backend.secondBucket.resources.bucket; +const cfnSecondaryBucket = s3SecondaryBucket.node.defaultChild as s3.CfnBucket; cfnBucket.accelerateConfiguration = { accelerationStatus: "Enabled", }; +cfnSecondaryBucket.accelerateConfiguration = { + accelerationStatus: "Enabled", +}; + +// required to add the metadata header, which amplify-backend does not support +backend.firstBucket.resources.cfnResources.cfnBucket.corsConfiguration = { + corsRules: [ + { + allowedHeaders: ["*"], + allowedMethods: ["GET", "HEAD", "PUT", "POST", "DELETE"], + allowedOrigins: ["*"], + exposedHeaders: [ + "x-amz-server-side-encryption", + "x-amz-request-id", + "x-amz-id-2", + "ETag", + "x-amz-meta-description", + ], + maxAge: 3000, + }, + ], +}; + // required to add the metadata header, which amplify-backend does not support -backend.storage.resources.cfnResources.cfnBucket.corsConfiguration = { +backend.secondBucket.resources.cfnResources.cfnBucket.corsConfiguration = { corsRules: [ { allowedHeaders: ["*"], diff --git a/infra-gen2/backends/storage/main/amplify/storage/resource.ts b/infra-gen2/backends/storage/main/amplify/storage/resource.ts index 3fb921c12b..4bb947fa19 100644 --- a/infra-gen2/backends/storage/main/amplify/storage/resource.ts +++ b/infra-gen2/backends/storage/main/amplify/storage/resource.ts @@ -1,7 +1,25 @@ import { defineStorage } from "@aws-amplify/backend"; -export const storage = defineStorage({ - name: "Storage Integ Test main", +export const firstBucket = defineStorage({ + name: "Storage Integ Test main bucket", + isDefault: true, + access: (allow) => ({ + "public/*": [ + allow.guest.to(["read", "write", "delete"]), + allow.authenticated.to(["read", "delete", "write"]), + ], + "protected/{entity_id}/*": [ + allow.authenticated.to(["read"]), + allow.entity("identity").to(["read", "write", "delete"]), + ], + "private/{entity_id}/*": [ + allow.entity("identity").to(["read", "write", "delete"]), + ], + }), +}); + +export const secondBucket = defineStorage({ + name: "Storage Integ Test secondary bucket", access: (allow) => ({ "public/*": [ allow.guest.to(["read", "write", "delete"]), diff --git a/infra-gen2/package-lock.json b/infra-gen2/package-lock.json index d518307579..af86191d84 100644 --- a/infra-gen2/package-lock.json +++ b/infra-gen2/package-lock.json @@ -24501,7 +24501,7 @@ "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.6", "get-stream": "^6.0.1", "human-signals": "^4.3.0", "is-stream": "^3.0.0", @@ -24982,7 +24982,7 @@ "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.6", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", @@ -25197,7 +25197,7 @@ "dev": true, "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -27936,7 +27936,7 @@ "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.6", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", diff --git a/infra-gen2/package.json b/infra-gen2/package.json index 0080a3855c..aaddb5abc8 100644 --- a/infra-gen2/package.json +++ b/infra-gen2/package.json @@ -28,6 +28,7 @@ "typescript": "^5.5.4" }, "overrides": { - "fast-xml-parser": "^4.4.1" + "fast-xml-parser": "^4.4.1", + "cross-spawn": "^7.0.6" } } diff --git a/infra/pnpm-lock.yaml b/infra/pnpm-lock.yaml index 456aa1cabb..110634935d 100644 --- a/infra/pnpm-lock.yaml +++ b/infra/pnpm-lock.yaml @@ -1181,8 +1181,8 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} data-uri-to-buffer@4.0.1: @@ -3830,7 +3830,7 @@ snapshots: create-require@1.1.1: {} - cross-spawn@7.0.3: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -3909,7 +3909,7 @@ snapshots: execa@5.1.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 6.0.1 human-signals: 2.1.0 is-stream: 2.0.1 diff --git a/packages/amplify/amplify_flutter/CHANGELOG.md b/packages/amplify/amplify_flutter/CHANGELOG.md index 345b2a77b9..32d429df1e 100644 --- a/packages/amplify/amplify_flutter/CHANGELOG.md +++ b/packages/amplify/amplify_flutter/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.6.0 + +### Features +- feat(storage): add multi-bucket feature support ([#5681](https://github.com/aws-amplify/amplify-flutter/pull/5681)) + +### Fixes +- fix(aws_common): removed JS streamed requests ([#5797](https://github.com/aws-amplify/amplify-flutter/pull/5797)) +- fix(datastore): properly handle multiple configures on Android ([#5740](https://github.com/aws-amplify/amplify-flutter/pull/5740)) + ## 2.5.0 ### Features diff --git a/packages/amplify/amplify_flutter/pubspec.yaml b/packages/amplify/amplify_flutter/pubspec.yaml index 738192e53c..c21f176751 100644 --- a/packages/amplify/amplify_flutter/pubspec.yaml +++ b/packages/amplify/amplify_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_flutter description: The top level Flutter package for the AWS Amplify libraries. -version: 2.5.0 +version: 2.6.0 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/amplify/amplify_flutter issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -19,7 +19,7 @@ platforms: web: dependencies: - amplify_core: ">=2.5.0 <2.6.0" + amplify_core: ">=2.6.0 <2.7.0" amplify_secure_storage: ">=0.5.7 <0.6.0" aws_common: ">=0.7.3 <0.8.0" collection: ^1.15.0 diff --git a/packages/amplify_core/CHANGELOG.md b/packages/amplify_core/CHANGELOG.md index de61e1a736..eeeba04989 100644 --- a/packages/amplify_core/CHANGELOG.md +++ b/packages/amplify_core/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.6.0 + +### Features +- feat(storage): add multi-bucket feature support ([#5681](https://github.com/aws-amplify/amplify-flutter/pull/5681)) + +### Fixes +- fix(aws_common): removed JS streamed requests ([#5797](https://github.com/aws-amplify/amplify-flutter/pull/5797)) + ## 2.5.0 ### Features diff --git a/packages/amplify_core/lib/src/config/amplify_outputs/storage/bucket_outputs.dart b/packages/amplify_core/lib/src/config/amplify_outputs/storage/bucket_outputs.dart new file mode 100644 index 0000000000..e156f08567 --- /dev/null +++ b/packages/amplify_core/lib/src/config/amplify_outputs/storage/bucket_outputs.dart @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_core/amplify_core.dart'; + +part 'bucket_outputs.g.dart'; + +/// {@template amplify_core.amplify_outputs.bucket_outputs} +/// The Amplify Gen 2 outputs for Buckets in the Storage category. +/// {@endtemplate} +@zAmplifyOutputsSerializable +class BucketOutputs + with AWSEquatable, AWSSerializable, AWSDebuggable { + /// {@macro amplify_core.amplify_outputs.bucket_outputs} + const BucketOutputs({ + required this.name, + required this.bucketName, + required this.awsRegion, + }); + + factory BucketOutputs.fromJson(Map json) => + _$BucketOutputsFromJson(json); + + /// The user friendly name of the bucket + final String name; + + /// The Amazon S3 bucket name. + final String bucketName; + + /// The AWS region of Amazon S3 resources. + final String awsRegion; + + @override + List get props => [ + name, + bucketName, + awsRegion, + ]; + + @override + String get runtimeTypeName => 'BucketOutputs'; + + @override + Object? toJson() { + return _$BucketOutputsToJson(this); + } +} diff --git a/packages/amplify_core/lib/src/config/amplify_outputs/storage/bucket_outputs.g.dart b/packages/amplify_core/lib/src/config/amplify_outputs/storage/bucket_outputs.g.dart new file mode 100644 index 0000000000..0d60c85fcc --- /dev/null +++ b/packages/amplify_core/lib/src/config/amplify_outputs/storage/bucket_outputs.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: deprecated_member_use_from_same_package + +part of 'bucket_outputs.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BucketOutputs _$BucketOutputsFromJson(Map json) => + $checkedCreate( + 'BucketOutputs', + json, + ($checkedConvert) { + final val = BucketOutputs( + name: $checkedConvert('name', (v) => v as String), + bucketName: $checkedConvert('bucket_name', (v) => v as String), + awsRegion: $checkedConvert('aws_region', (v) => v as String), + ); + return val; + }, + fieldKeyMap: const { + 'bucketName': 'bucket_name', + 'awsRegion': 'aws_region' + }, + ); + +Map _$BucketOutputsToJson(BucketOutputs instance) => + { + 'name': instance.name, + 'bucket_name': instance.bucketName, + 'aws_region': instance.awsRegion, + }; diff --git a/packages/amplify_core/lib/src/config/amplify_outputs/storage/storage_outputs.dart b/packages/amplify_core/lib/src/config/amplify_outputs/storage/storage_outputs.dart index 9db9356aa6..fec0ec0662 100644 --- a/packages/amplify_core/lib/src/config/amplify_outputs/storage/storage_outputs.dart +++ b/packages/amplify_core/lib/src/config/amplify_outputs/storage/storage_outputs.dart @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import 'package:amplify_core/amplify_core.dart'; +import 'package:amplify_core/src/config/amplify_outputs/storage/bucket_outputs.dart'; part 'storage_outputs.g.dart'; @@ -12,7 +13,11 @@ part 'storage_outputs.g.dart'; class StorageOutputs with AWSEquatable, AWSSerializable, AWSDebuggable { /// {@macro amplify_core.amplify_outputs.storage_outputs} - const StorageOutputs({required this.awsRegion, required this.bucketName}); + const StorageOutputs({ + required this.awsRegion, + required this.bucketName, + this.buckets, + }); factory StorageOutputs.fromJson(Map json) => _$StorageOutputsFromJson(json); @@ -23,8 +28,11 @@ class StorageOutputs /// The Amazon S3 bucket name. final String bucketName; + /// The list of buckets if there are multiple buckets for the project + final List? buckets; + @override - List get props => [awsRegion, bucketName]; + List get props => [awsRegion, bucketName, buckets]; @override String get runtimeTypeName => 'StorageOutputs'; diff --git a/packages/amplify_core/lib/src/config/amplify_outputs/storage/storage_outputs.g.dart b/packages/amplify_core/lib/src/config/amplify_outputs/storage/storage_outputs.g.dart index 7b90421189..40d147f387 100644 --- a/packages/amplify_core/lib/src/config/amplify_outputs/storage/storage_outputs.g.dart +++ b/packages/amplify_core/lib/src/config/amplify_outputs/storage/storage_outputs.g.dart @@ -16,6 +16,12 @@ StorageOutputs _$StorageOutputsFromJson(Map json) => final val = StorageOutputs( awsRegion: $checkedConvert('aws_region', (v) => v as String), bucketName: $checkedConvert('bucket_name', (v) => v as String), + buckets: $checkedConvert( + 'buckets', + (v) => (v as List?) + ?.map( + (e) => BucketOutputs.fromJson(e as Map)) + .toList()), ); return val; }, @@ -25,8 +31,18 @@ StorageOutputs _$StorageOutputsFromJson(Map json) => }, ); -Map _$StorageOutputsToJson(StorageOutputs instance) => - { - 'aws_region': instance.awsRegion, - 'bucket_name': instance.bucketName, - }; +Map _$StorageOutputsToJson(StorageOutputs instance) { + final val = { + 'aws_region': instance.awsRegion, + 'bucket_name': instance.bucketName, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('buckets', instance.buckets?.map((e) => e.toJson()).toList()); + return val; +} diff --git a/packages/amplify_core/lib/src/types/auth/attribute/auth_user_attribute_key.dart b/packages/amplify_core/lib/src/types/auth/attribute/auth_user_attribute_key.dart index a17900049e..04064286cf 100644 --- a/packages/amplify_core/lib/src/types/auth/attribute/auth_user_attribute_key.dart +++ b/packages/amplify_core/lib/src/types/auth/attribute/auth_user_attribute_key.dart @@ -9,7 +9,6 @@ import 'package:meta/meta.dart'; /// {@template amplify_core.auth_user_attribute_key} /// A user attribute identifier. /// {@endtemplate} -/// {@hideConstantImplementations} @immutable abstract class AuthUserAttributeKey with diff --git a/packages/amplify_core/lib/src/types/auth/attribute/cognito_user_attribute_key.dart b/packages/amplify_core/lib/src/types/auth/attribute/cognito_user_attribute_key.dart index 21e4867dae..ebda0d423c 100644 --- a/packages/amplify_core/lib/src/types/auth/attribute/cognito_user_attribute_key.dart +++ b/packages/amplify_core/lib/src/types/auth/attribute/cognito_user_attribute_key.dart @@ -12,7 +12,6 @@ import 'package:amplify_core/amplify_core.dart'; /// [here](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html). /// /// Use [CognitoUserAttributeKey.custom] to create a custom Cognito attribute. -/// {@hideConstantImplementations} class CognitoUserAttributeKey extends AuthUserAttributeKey { const CognitoUserAttributeKey._(this._key, {this.readOnly = false}) : isCustom = false; diff --git a/packages/amplify_core/lib/src/types/exception/amplify_exception.dart b/packages/amplify_core/lib/src/types/exception/amplify_exception.dart index aedc1c1e19..a60f435bd6 100644 --- a/packages/amplify_core/lib/src/types/exception/amplify_exception.dart +++ b/packages/amplify_core/lib/src/types/exception/amplify_exception.dart @@ -21,6 +21,7 @@ part 'network_exception.dart'; part 'push/push_notification_exception.dart'; part 'storage/access_denied_exception.dart'; part 'storage/http_status_exception.dart'; +part 'storage/invalid_storage_bucket_exception.dart'; part 'storage/local_file_not_found_exception.dart'; part 'storage/not_found_exception.dart'; part 'storage/operation_canceled_exception.dart'; diff --git a/packages/amplify_core/lib/src/types/exception/storage/invalid_storage_bucket_exception.dart b/packages/amplify_core/lib/src/types/exception/storage/invalid_storage_bucket_exception.dart new file mode 100644 index 0000000000..5e5c56fe0e --- /dev/null +++ b/packages/amplify_core/lib/src/types/exception/storage/invalid_storage_bucket_exception.dart @@ -0,0 +1,15 @@ +part of '../amplify_exception.dart'; + +/// {@template amplify_core.storage.invalid_storage_bucket_exception} +/// Exception thrown when the [StorageBucket] is invalid. +/// {@endtemplate} +class InvalidStorageBucketException extends StorageException { + const InvalidStorageBucketException( + super.message, { + super.recoverySuggestion, + super.underlyingException, + }); + + @override + String get runtimeTypeName => 'InvalidStorageBucketException'; +} diff --git a/packages/amplify_core/lib/src/types/storage/bucket_info.dart b/packages/amplify_core/lib/src/types/storage/bucket_info.dart new file mode 100644 index 0000000000..811871f9b3 --- /dev/null +++ b/packages/amplify_core/lib/src/types/storage/bucket_info.dart @@ -0,0 +1,24 @@ +import 'package:amplify_core/amplify_core.dart'; + +/// {@template amplify_core.storage.bucket_info} +/// Presents a storage bucket information. +/// {@endtemplate} +class BucketInfo + with AWSEquatable, AWSSerializable> { + /// {@macro amplify_core.storage.bucket_info} + const BucketInfo({required this.bucketName, required this.region}); + final String bucketName; + final String region; + + @override + List get props => [ + bucketName, + region, + ]; + + @override + Map toJson() => { + 'bucketName': bucketName, + 'region': region, + }; +} diff --git a/packages/amplify_core/lib/src/types/storage/copy_buckets.dart b/packages/amplify_core/lib/src/types/storage/copy_buckets.dart new file mode 100644 index 0000000000..7bfb83575d --- /dev/null +++ b/packages/amplify_core/lib/src/types/storage/copy_buckets.dart @@ -0,0 +1,24 @@ +import 'package:amplify_core/amplify_core.dart'; + +/// Presents storage buckets for a copy operation. +class CopyBuckets with AWSSerializable> { + /// Creates a [CopyBuckets] with [source] and [destination] buckets. + const CopyBuckets({ + required this.source, + required this.destination, + }); + + /// Creates a [CopyBuckets] with the same [bucket] for the [source] and [destination]. + CopyBuckets.sameBucket(StorageBucket bucket) + : source = bucket, + destination = bucket; + + final StorageBucket source; + final StorageBucket destination; + + @override + Map toJson() => { + 'source': source.toJson(), + 'destination': destination.toJson(), + }; +} diff --git a/packages/amplify_core/lib/src/types/storage/copy_options.dart b/packages/amplify_core/lib/src/types/storage/copy_options.dart index eacd7f4ff1..a9910ecfff 100644 --- a/packages/amplify_core/lib/src/types/storage/copy_options.dart +++ b/packages/amplify_core/lib/src/types/storage/copy_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.copy_options} /// Configurable options for `Amplify.Storage.copy`. @@ -12,13 +12,19 @@ class StorageCopyOptions AWSSerializable>, AWSDebuggable { /// {@macro amplify_core.storage.copy_options} - const StorageCopyOptions({this.pluginOptions}); + const StorageCopyOptions({ + this.pluginOptions, + this.buckets, + }); /// plugin specific options for `Amplify.Storage.copy`. final StorageCopyPluginOptions? pluginOptions; + /// Optionally specify which buckets to target + final CopyBuckets? buckets; + @override - List get props => [pluginOptions]; + List get props => [pluginOptions, buckets]; @override String get runtimeTypeName => 'StorageCopyOptions'; @@ -26,6 +32,7 @@ class StorageCopyOptions @override Map toJson() => { 'pluginOptions': pluginOptions?.toJson(), + 'buckets': buckets?.toJson(), }; } diff --git a/packages/amplify_core/lib/src/types/storage/download_data_options.dart b/packages/amplify_core/lib/src/types/storage/download_data_options.dart index 25c59523f1..26a4ea2422 100644 --- a/packages/amplify_core/lib/src/types/storage/download_data_options.dart +++ b/packages/amplify_core/lib/src/types/storage/download_data_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.download_data_options} /// Configurable options for `Amplify.Storage.downloadData`. @@ -14,13 +14,17 @@ class StorageDownloadDataOptions /// {@macro amplify_core.storage.download_data_options} const StorageDownloadDataOptions({ this.pluginOptions, + this.bucket, }); /// {@macro amplify_core.storage.download_data_plugin_options} final StorageDownloadDataPluginOptions? pluginOptions; + /// Optionally specify which bucket to target + final StorageBucket? bucket; + @override - List get props => [pluginOptions]; + List get props => [pluginOptions, bucket]; @override String get runtimeTypeName => 'StorageDownloadDataOptions'; @@ -28,6 +32,7 @@ class StorageDownloadDataOptions @override Map toJson() => { 'pluginOptions': pluginOptions?.toJson(), + 'bucket': bucket?.toJson(), }; } diff --git a/packages/amplify_core/lib/src/types/storage/download_file_options.dart b/packages/amplify_core/lib/src/types/storage/download_file_options.dart index 8681667131..69be1758cd 100644 --- a/packages/amplify_core/lib/src/types/storage/download_file_options.dart +++ b/packages/amplify_core/lib/src/types/storage/download_file_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.download_file_options} /// Configurable options for `Amplify.Storage.downloadFile`. @@ -14,13 +14,17 @@ class StorageDownloadFileOptions /// {@macro amplify_core.storage.download_file_options} const StorageDownloadFileOptions({ this.pluginOptions, + this.bucket, }); /// {@macro amplify_core.storage.download_file_plugin_options} final StorageDownloadFilePluginOptions? pluginOptions; + /// Optionally specify which bucket to target + final StorageBucket? bucket; + @override - List get props => [pluginOptions]; + List get props => [pluginOptions, bucket]; @override String get runtimeTypeName => 'StorageDownloadFileOptions'; @@ -28,6 +32,7 @@ class StorageDownloadFileOptions @override Map toJson() => { 'pluginOptions': pluginOptions?.toJson(), + 'bucket': bucket?.toJson(), }; } diff --git a/packages/amplify_core/lib/src/types/storage/get_properties_options.dart b/packages/amplify_core/lib/src/types/storage/get_properties_options.dart index bef609f7a7..9a2216b40d 100644 --- a/packages/amplify_core/lib/src/types/storage/get_properties_options.dart +++ b/packages/amplify_core/lib/src/types/storage/get_properties_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.get_properties_options} /// Configurable options for `Amplify.Storage.getProperties`. @@ -14,13 +14,17 @@ class StorageGetPropertiesOptions /// {@macro amplify_core.storage.get_properties_options} const StorageGetPropertiesOptions({ this.pluginOptions, + this.bucket, }); /// {@macro amplify_core.storage.download_get_properties_plugin_options} final StorageGetPropertiesPluginOptions? pluginOptions; + /// Optionally specify which bucket to retrieve + final StorageBucket? bucket; + @override - List get props => [pluginOptions]; + List get props => [pluginOptions, bucket]; @override String get runtimeTypeName => 'StorageGetPropertiesOptions'; @@ -28,6 +32,7 @@ class StorageGetPropertiesOptions @override Map toJson() => { 'pluginOptions': pluginOptions?.toJson(), + 'bucket': bucket?.toJson(), }; } diff --git a/packages/amplify_core/lib/src/types/storage/get_url_options.dart b/packages/amplify_core/lib/src/types/storage/get_url_options.dart index 3f4078839e..161cc2b93b 100644 --- a/packages/amplify_core/lib/src/types/storage/get_url_options.dart +++ b/packages/amplify_core/lib/src/types/storage/get_url_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.get_url_options} /// Configurable options for `Amplify.Storage.getUrl`. @@ -14,13 +14,20 @@ class StorageGetUrlOptions /// {@macro amplify_core.storage.get_url_options} const StorageGetUrlOptions({ this.pluginOptions, + this.bucket, }); /// {@macro amplify_core.storage.get_url_plugin_options} final StorageGetUrlPluginOptions? pluginOptions; + /// Optionally specify which bucket to target + final StorageBucket? bucket; + @override - List get props => [pluginOptions]; + List get props => [ + pluginOptions, + bucket, + ]; @override String get runtimeTypeName => 'StorageGetUrlOptions'; @@ -28,6 +35,7 @@ class StorageGetUrlOptions @override Map toJson() => { 'pluginOptions': pluginOptions?.toJson(), + 'bucket': bucket?.toJson(), }; } 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 c046f90d55..72570f262b 100644 --- a/packages/amplify_core/lib/src/types/storage/list_options.dart +++ b/packages/amplify_core/lib/src/types/storage/list_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.list_options} /// Configurable options for `Amplify.Storage.list`. @@ -15,6 +15,7 @@ class StorageListOptions const StorageListOptions({ this.pageSize = 1000, this.nextToken, + this.bucket, this.pluginOptions, }); @@ -27,8 +28,11 @@ class StorageListOptions /// {@macro amplify_core.storage.list_plugin_options} final StorageListPluginOptions? pluginOptions; + /// Optionally specify which bucket to retrieve + final StorageBucket? bucket; + @override - List get props => [pageSize, nextToken, pluginOptions]; + List get props => [pageSize, nextToken, pluginOptions, bucket]; @override String get runtimeTypeName => 'StorageListOptions'; @@ -37,6 +41,7 @@ class StorageListOptions Map toJson() => { 'pageSize': pageSize, 'nextToken': nextToken, + 'bucket': bucket?.toJson(), 'pluginOptions': pluginOptions?.toJson(), }; } diff --git a/packages/amplify_core/lib/src/types/storage/remove_many_options.dart b/packages/amplify_core/lib/src/types/storage/remove_many_options.dart index d9e32131fe..a50c651fc8 100644 --- a/packages/amplify_core/lib/src/types/storage/remove_many_options.dart +++ b/packages/amplify_core/lib/src/types/storage/remove_many_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_many_options} /// Configurable options for `Amplify.Storage.removeMany`. @@ -14,13 +14,20 @@ class StorageRemoveManyOptions /// {@macro amplify_core.storage.remove_many_options} const StorageRemoveManyOptions({ this.pluginOptions, + this.bucket, }); /// {@macro amplify_core.storage.remove_many_plugin_options} final StorageRemoveManyPluginOptions? pluginOptions; + /// Optionally specify which bucket to target + final StorageBucket? bucket; + @override - List get props => [pluginOptions]; + List get props => [ + pluginOptions, + bucket, + ]; @override String get runtimeTypeName => 'StorageRemoveManyOptions'; @@ -28,6 +35,7 @@ class StorageRemoveManyOptions @override Map toJson() => { 'pluginOptions': pluginOptions?.toJson(), + 'bucket': bucket?.toJson(), }; } 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..13e68cb501 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?.toJson(), }; } diff --git a/packages/amplify_core/lib/src/types/storage/storage_bucket.dart b/packages/amplify_core/lib/src/types/storage/storage_bucket.dart new file mode 100644 index 0000000000..ccfdd345a9 --- /dev/null +++ b/packages/amplify_core/lib/src/types/storage/storage_bucket.dart @@ -0,0 +1,24 @@ +import 'package:amplify_core/amplify_core.dart'; +import 'package:amplify_core/src/config/amplify_outputs/storage/storage_outputs.dart'; +import 'package:amplify_core/src/types/storage/storage_bucket_from_outputs.dart'; +import 'package:meta/meta.dart'; + +/// Presents a storage bucket. +class StorageBucket with AWSSerializable> { + /// Creates a [StorageBucket] from [BucketInfo]. + const StorageBucket.fromBucketInfo(this._info); + + /// Creates a [StorageBucket] defined by the [name] in AmplifyOutputs file. + factory StorageBucket.fromOutputs(String name) => + StorageBucketFromOutputs(name); + + final BucketInfo _info; + + @internal + BucketInfo resolveBucketInfo(StorageOutputs? storageOutputs) => _info; + + @override + Map toJson() => { + '_info': _info.toJson(), + }; +} diff --git a/packages/amplify_core/lib/src/types/storage/storage_bucket_from_outputs.dart b/packages/amplify_core/lib/src/types/storage/storage_bucket_from_outputs.dart new file mode 100644 index 0000000000..b51d17029d --- /dev/null +++ b/packages/amplify_core/lib/src/types/storage/storage_bucket_from_outputs.dart @@ -0,0 +1,48 @@ +import 'package:amplify_core/amplify_core.dart'; +import 'package:amplify_core/src/config/amplify_outputs/storage/storage_outputs.dart'; +import 'package:meta/meta.dart'; + +/// {@template amplify_core.storage.storage_bucket_from_outputs} +/// Creates a [StorageBucket] defined by the name in AmplifyOutputs file. +/// {@endtemplate} +@internal +class StorageBucketFromOutputs implements StorageBucket { + /// {@macro amplify_core.storage.storage_bucket_from_outputs} + const StorageBucketFromOutputs(this._name); + + final String _name; + + @override + BucketInfo resolveBucketInfo(StorageOutputs? storageOutputs) { + assert( + storageOutputs != null, + 'storageOutputs can not be null', + ); + 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.', + ), + ); + return BucketInfo( + bucketName: bucket.bucketName, + region: bucket.awsRegion, + ); + } + + @override + Map toJson() => { + '_name': _name, + }; +} 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 9324a72683..b00b66a362 100644 --- a/packages/amplify_core/lib/src/types/storage/storage_types.dart +++ b/packages/amplify_core/lib/src/types/storage/storage_types.dart @@ -11,6 +11,8 @@ export '../exception/amplify_exception.dart' StorageOperationCanceledException, NetworkException, UnknownException; +export 'bucket_info.dart'; +export 'copy_buckets.dart'; export 'copy_operation.dart'; export 'copy_options.dart'; export 'copy_request.dart'; @@ -44,6 +46,7 @@ export 'remove_operation.dart'; export 'remove_options.dart'; export 'remove_request.dart'; export 'remove_result.dart'; +export 'storage_bucket.dart'; export 'storage_item.dart'; export 'storage_path.dart'; export 'transfer_progress.dart'; diff --git a/packages/amplify_core/lib/src/types/storage/upload_data_options.dart b/packages/amplify_core/lib/src/types/storage/upload_data_options.dart index eb997552de..83e8f5009c 100644 --- a/packages/amplify_core/lib/src/types/storage/upload_data_options.dart +++ b/packages/amplify_core/lib/src/types/storage/upload_data_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.upload_data_options} /// Configurable options for `Amplify.Storage.uploadData`. @@ -15,16 +15,20 @@ class StorageUploadDataOptions const StorageUploadDataOptions({ this.metadata = const {}, this.pluginOptions, + this.bucket, }); /// The metadata attached to the object to be uploaded. final Map metadata; + /// Optionally specify which bucket to target. + final StorageBucket? bucket; + /// {@macro amplify_core.storage.upload_data_plugin_options} final StorageUploadDataPluginOptions? pluginOptions; @override - List get props => [metadata, pluginOptions]; + List get props => [metadata, pluginOptions, bucket]; @override String get runtimeTypeName => 'StorageUploadDataOptions'; @@ -33,6 +37,7 @@ class StorageUploadDataOptions Map toJson() => { 'metadata': metadata, 'pluginOptions': pluginOptions?.toJson(), + 'bucket': bucket?.toJson(), }; } diff --git a/packages/amplify_core/lib/src/types/storage/upload_file_options.dart b/packages/amplify_core/lib/src/types/storage/upload_file_options.dart index bad1529468..2422eef043 100644 --- a/packages/amplify_core/lib/src/types/storage/upload_file_options.dart +++ b/packages/amplify_core/lib/src/types/storage/upload_file_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.upload_file_options} /// Configurable options for `Amplify.Storage.uploadFile`. @@ -15,6 +15,7 @@ class StorageUploadFileOptions const StorageUploadFileOptions({ this.metadata = const {}, this.pluginOptions, + this.bucket, }); /// The metadata attached to the object to be uploaded. @@ -23,8 +24,11 @@ class StorageUploadFileOptions /// {@macro amplify_core.storage.upload_file_plugin_options} final StorageUploadFilePluginOptions? pluginOptions; + /// Optionally specify which bucket to target + final StorageBucket? bucket; + @override - List get props => [metadata, pluginOptions]; + List get props => [metadata, pluginOptions, bucket]; @override String get runtimeTypeName => 'StorageUploadFileOptions'; @@ -33,6 +37,7 @@ class StorageUploadFileOptions Map toJson() => { 'metadata': metadata, 'pluginOptions': pluginOptions?.toJson(), + 'bucket': bucket?.toJson(), }; } diff --git a/packages/amplify_core/pubspec.yaml b/packages/amplify_core/pubspec.yaml index f6f0d4195e..12e991bfdb 100644 --- a/packages/amplify_core/pubspec.yaml +++ b/packages/amplify_core/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_core description: The base package containing common types and utilities that are shared across the Amplify Flutter packages. -version: 2.5.0 +version: 2.6.0 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/amplify_core issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -10,7 +10,7 @@ environment: dependencies: async: ^2.10.0 - aws_common: ">=0.7.4 <0.8.0" + aws_common: ">=0.7.5 <0.8.0" aws_signature_v4: ">=0.6.3 <0.7.0" collection: ^1.15.0 graphs: ^2.1.0 diff --git a/packages/amplify_core/test/config/amplify_outputs/test_data.dart b/packages/amplify_core/test/config/amplify_outputs/test_data.dart index 317e7a78b4..887e46a260 100644 --- a/packages/amplify_core/test/config/amplify_outputs/test_data.dart +++ b/packages/amplify_core/test/config/amplify_outputs/test_data.dart @@ -5,8 +5,8 @@ // It uses json-schema-faker to generate a sample json from the Amplify GEN 2 client-config-schema. // Run below commands to regenerate a sample json. -// curl https://raw.githubusercontent.com/aws-amplify/amplify-backend/main/packages/client-config/src/client-config-schema/schema_v1.json -o schema_v1.json -// npx json-schema-faker -s schema_v1.json -o sample.json --alwaysFakeOptionals +// curl https://raw.githubusercontent.com/aws-amplify/amplify-backend/main/packages/client-config/src/client-config-schema/schema_v1.3.json -o schema_v1.3.json +// npx json-schema-faker -s schema_v1.3.json -o sample.json --alwaysFakeOptionals const amplifyoutputs = '''{ "schema": "dolor nisi incididunt adipisicing", @@ -99,9 +99,26 @@ const amplifyoutputs = '''{ ] }, "storage": { - "aws_region": "oem dks", - "bucket_name": "dolor et esse" - }, + "aws_region": "mollit culpa non dolore sint", + "bucket_name": "incididunt minim nulla", + "buckets": [ + { + "aws_region": "mollit culpa non dolore sint", + "bucket_name": "incididunt minim nulla", + "name": "ullamco consectetur dolore" + }, + { + "aws_region": "Duis commodo", + "bucket_name": "sint", + "name": "ex non" + }, + { + "aws_region": "enim cillum eiusmod", + "bucket_name": "proident ullamco deserunt", + "name": "minim elit" + } + ] +}, "version": "1" } '''; diff --git a/packages/amplify_core/test/config/amplify_outputs_mapping/app/package-lock.json b/packages/amplify_core/test/config/amplify_outputs_mapping/app/package-lock.json index e831d6c60d..e28001149e 100644 --- a/packages/amplify_core/test/config/amplify_outputs_mapping/app/package-lock.json +++ b/packages/amplify_core/test/config/amplify_outputs_mapping/app/package-lock.json @@ -15357,9 +15357,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -15530,7 +15530,7 @@ "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.6", "get-stream": "^6.0.1", "human-signals": "^4.3.0", "is-stream": "^3.0.0", @@ -15980,7 +15980,7 @@ "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.6", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", @@ -16196,7 +16196,7 @@ "dev": true, "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -18810,7 +18810,7 @@ "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.6", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", diff --git a/packages/amplify_core/test/config/amplify_outputs_mapping/app/package.json b/packages/amplify_core/test/config/amplify_outputs_mapping/app/package.json index 6edabe5ed0..7b648cfcae 100644 --- a/packages/amplify_core/test/config/amplify_outputs_mapping/app/package.json +++ b/packages/amplify_core/test/config/amplify_outputs_mapping/app/package.json @@ -18,6 +18,10 @@ "@aws-amplify/backend-cli": "1.0.4", "tsx": "^4.10.5", "typescript": "^5.4.5" + + }, + "overrides": { + "cross-spawn": "^7.0.6" } } \ No newline at end of file diff --git a/packages/amplify_core/test/types/storage/storage_bucket_test.dart b/packages/amplify_core/test/types/storage/storage_bucket_test.dart new file mode 100644 index 0000000000..edae2ab4af --- /dev/null +++ b/packages/amplify_core/test/types/storage/storage_bucket_test.dart @@ -0,0 +1,87 @@ +import 'package:amplify_core/amplify_core.dart'; +import 'package:amplify_core/src/config/amplify_outputs/storage/bucket_outputs.dart'; +import 'package:amplify_core/src/config/amplify_outputs/storage/storage_outputs.dart'; +import 'package:test/test.dart'; + +void main() { + group('Storage bucket resolve BucketInfo', () { + const defaultBucketOutputs = BucketOutputs( + name: 'default-bucket-friendly-name', + bucketName: 'default-bucket-unique-name', + awsRegion: 'default-bucket-aws-region', + ); + const secondBucketOutputs = BucketOutputs( + name: 'second-bucket-friendly-name', + bucketName: 'second-bucket-unique-name', + awsRegion: 'second-bucket-aws-region', + ); + final defaultBucketInfo = BucketInfo( + bucketName: defaultBucketOutputs.bucketName, + region: defaultBucketOutputs.awsRegion, + ); + final secondBucketInfo = BucketInfo( + bucketName: secondBucketOutputs.bucketName, + region: secondBucketOutputs.awsRegion, + ); + final testStorageOutputsMultiBucket = StorageOutputs( + awsRegion: defaultBucketOutputs.awsRegion, + bucketName: defaultBucketOutputs.bucketName, + buckets: [ + defaultBucketOutputs, + secondBucketOutputs, + ], + ); + final testStorageOutputsSingleBucket = StorageOutputs( + awsRegion: defaultBucketOutputs.awsRegion, + bucketName: defaultBucketOutputs.bucketName, + ); + + test( + 'should return same bucket info when storage bucket is created from' + ' a bucket info', () { + final storageBucket = StorageBucket.fromBucketInfo( + defaultBucketInfo, + ); + final bucketInfo = storageBucket.resolveBucketInfo(null); + expect(bucketInfo, defaultBucketInfo); + }); + + test( + 'should return bucket info when storage bucket is created from' + ' buckets in storage outputs', () { + final storageBucket = StorageBucket.fromOutputs(secondBucketOutputs.name); + final bucketInfo = + storageBucket.resolveBucketInfo(testStorageOutputsMultiBucket); + expect(bucketInfo, secondBucketInfo); + }); + + test( + 'should throw assertion error when storage bucket is created from' + ' outputs and storage outputs is null', () { + final storageBucket = + StorageBucket.fromOutputs(defaultBucketOutputs.name); + expect( + () => storageBucket.resolveBucketInfo(null), + throwsA(isA()), + ); + }); + test( + 'should throw exception when storage bucket is created from outputs and' + ' storage outputs does not have buckets', () { + final storageBucket = StorageBucket.fromOutputs('bucket-name'); + expect( + () => storageBucket.resolveBucketInfo(testStorageOutputsSingleBucket), + throwsA(isA()), + ); + }); + test( + 'should throw exception when storage bucket is created from outputs and' + ' bucket name does not match any bucket in storage outputs', () { + final storageBucket = StorageBucket.fromOutputs('invalid-bucket-name'); + expect( + () => storageBucket.resolveBucketInfo(testStorageOutputsMultiBucket), + throwsA(isA()), + ); + }); + }); +} diff --git a/packages/amplify_datastore/CHANGELOG.md b/packages/amplify_datastore/CHANGELOG.md index 6d260c1adf..8bdca91393 100644 --- a/packages/amplify_datastore/CHANGELOG.md +++ b/packages/amplify_datastore/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.6.0 + +### Fixes +- fix(datastore): properly handle multiple configures on Android ([#5740](https://github.com/aws-amplify/amplify-flutter/pull/5740)) + ## 2.5.0 - Minor bug fixes and improvements diff --git a/packages/amplify_datastore/android/src/main/kotlin/com/amazonaws/amplify/amplify_datastore/AmplifyDataStorePlugin.kt b/packages/amplify_datastore/android/src/main/kotlin/com/amazonaws/amplify/amplify_datastore/AmplifyDataStorePlugin.kt index 803a4f764c..f6219475de 100644 --- a/packages/amplify_datastore/android/src/main/kotlin/com/amazonaws/amplify/amplify_datastore/AmplifyDataStorePlugin.kt +++ b/packages/amplify_datastore/android/src/main/kotlin/com/amazonaws/amplify/amplify_datastore/AmplifyDataStorePlugin.kt @@ -85,7 +85,7 @@ class AmplifyDataStorePlugin : NativeApiBridge { private lateinit var channel: MethodChannel private lateinit var eventChannel: EventChannel - private lateinit var observeCancelable: Cancelable + private var observeCancelable: Cancelable? = null private lateinit var hubEventChannel: EventChannel private val dataStoreObserveEventStreamHandler: DataStoreObserveEventStreamHandler @@ -93,7 +93,6 @@ class AmplifyDataStorePlugin : private val uiThreadHandler: Handler private val LOG = Amplify.Logging.forNamespace("amplify:flutter:datastore") private var isSettingUpObserve = AtomicBoolean() - private var nativeAuthPlugin: NativeAuthPlugin? = null private var nativeApiPlugin: NativeApiPlugin? = null private val coroutineScope = CoroutineScope(CoroutineName("AmplifyFlutterPlugin")) private val dispatcher: CoroutineDispatcher = Dispatchers.IO @@ -114,6 +113,8 @@ class AmplifyDataStorePlugin : * be instantiated only once but still maintain a reference to the active method channel. */ var flutterAuthProviders: FlutterAuthProviders? = null + var nativeAuthPlugin: NativeAuthPlugin? = null + var hasAddedUserAgent :Boolean = false } val modelProvider = FlutterModelProvider.instance @@ -185,6 +186,14 @@ class AmplifyDataStorePlugin : override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) + hubEventChannel.setStreamHandler(null) + + observeCancelable?.cancel() + observeCancelable = null + + dataStoreHubEventStreamHandler.onCancel(null) + nativeAuthPlugin = null NativeAuthBridge.setUp(binding.binaryMessenger, null) @@ -438,7 +447,7 @@ class AmplifyDataStorePlugin : queryPredicate = QueryPredicateBuilder.fromSerializedMap( request["queryPredicate"].safeCastToMap() - ) ?: QueryPredicates.all() + ) ?: QueryPredicates.all() } catch (e: Exception) { uiThreadHandler.post { postExceptionToFlutterChannel( @@ -502,7 +511,7 @@ class AmplifyDataStorePlugin : } fun onSetUpObserve(flutterResult: Result) { - if (this::observeCancelable.isInitialized || isSettingUpObserve.getAndSet(true)) { + if (observeCancelable != null || isSettingUpObserve.getAndSet(true)) { flutterResult.success(true) return } @@ -932,6 +941,17 @@ class AmplifyDataStorePlugin : throw NotImplementedError("Not yet implemented") } + fun addUserAgent( + version: String, + ) { + if(hasAddedUserAgent) return + + @OptIn(AmplifyFlutterApi::class) + Amplify.addUserAgentPlatform(UserAgent.Platform.FLUTTER, "$version /datastore") + + hasAddedUserAgent = true + } + override fun configure( version: String, config: String, @@ -939,9 +959,9 @@ class AmplifyDataStorePlugin : ) { coroutineScope.launch(dispatcher) { try { - @OptIn(AmplifyFlutterApi::class) - Amplify.addUserAgentPlatform(UserAgent.Platform.FLUTTER, "$version /datastore") + addUserAgent(version) Amplify.configure(AmplifyOutputs(config), context) + withContext(Dispatchers.Main) { callback(kotlin.Result.success(Unit)) } diff --git a/packages/amplify_datastore/lib/src/utils/native_api_helpers.dart b/packages/amplify_datastore/lib/src/utils/native_api_helpers.dart index b59aceb856..cfdaf14fc3 100644 --- a/packages/amplify_datastore/lib/src/utils/native_api_helpers.dart +++ b/packages/amplify_datastore/lib/src/utils/native_api_helpers.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:amplify_core/amplify_core.dart'; import 'package:amplify_datastore/src/native_plugin.g.dart'; -import 'package:collection/collection.dart'; /// Convert a [NativeGraphQLResponse] to a [GraphQLResponse] GraphQLRequest nativeRequestToGraphQLRequest( @@ -85,7 +84,7 @@ NativeGraphQLResponse graphQLResponseToNativeResponse( /// from a [GraphQLResponse] String _buildPayloadJson(GraphQLResponse response) { final data = jsonDecode(response.data ?? '{}'); - final errors = response.errors.whereNotNull().map((e) => e.toJson()).toList(); + final errors = response.errors.nonNulls.map((e) => e.toJson()).toList(); return jsonEncode({ 'data': data, 'errors': errors, diff --git a/packages/amplify_datastore/pubspec.yaml b/packages/amplify_datastore/pubspec.yaml index 5d5ddc8948..c39d9b3112 100644 --- a/packages/amplify_datastore/pubspec.yaml +++ b/packages/amplify_datastore/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_datastore description: The Amplify Flutter DataStore category plugin, providing a queryable, on-device data store. -version: 2.5.0 +version: 2.6.0 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/amplify_datastore issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -12,8 +12,8 @@ environment: dependencies: flutter: sdk: flutter - amplify_datastore_plugin_interface: ">=2.5.0 <2.6.0" - amplify_core: ">=2.5.0 <2.6.0" + amplify_datastore_plugin_interface: ">=2.6.0 <2.7.0" + amplify_core: ">=2.6.0 <2.7.0" plugin_platform_interface: ^2.0.0 meta: ^1.7.0 collection: ^1.14.13 diff --git a/packages/amplify_datastore_plugin_interface/CHANGELOG.md b/packages/amplify_datastore_plugin_interface/CHANGELOG.md index 0a11b5216c..c2c5eea76f 100644 --- a/packages/amplify_datastore_plugin_interface/CHANGELOG.md +++ b/packages/amplify_datastore_plugin_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +- Minor bug fixes and improvements + ## 2.5.0 - Minor bug fixes and improvements diff --git a/packages/amplify_datastore_plugin_interface/pubspec.yaml b/packages/amplify_datastore_plugin_interface/pubspec.yaml index 82ea1e8838..60b19a31b0 100644 --- a/packages/amplify_datastore_plugin_interface/pubspec.yaml +++ b/packages/amplify_datastore_plugin_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_datastore_plugin_interface description: The platform interface for the DataStore module of Amplify Flutter. -version: 2.5.0 +version: 2.6.0 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/amplify_datastore_plugin_interface issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -10,7 +10,7 @@ environment: flutter: ">=3.24.0" dependencies: - amplify_core: ">=2.5.0 <2.6.0" + amplify_core: ">=2.6.0 <2.7.0" collection: ^1.15.0 flutter: sdk: flutter diff --git a/packages/analytics/amplify_analytics_pinpoint/CHANGELOG.md b/packages/analytics/amplify_analytics_pinpoint/CHANGELOG.md index 25bca8683a..f6cf29bbed 100644 --- a/packages/analytics/amplify_analytics_pinpoint/CHANGELOG.md +++ b/packages/analytics/amplify_analytics_pinpoint/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.6.0 + +- Minor bug fixes and improvements + +## 2.5.1 + +- Minor bug fixes and improvements + ## 2.5.0 - Minor bug fixes and improvements diff --git a/packages/analytics/amplify_analytics_pinpoint/pubspec.yaml b/packages/analytics/amplify_analytics_pinpoint/pubspec.yaml index 04219169f4..f962762d71 100644 --- a/packages/analytics/amplify_analytics_pinpoint/pubspec.yaml +++ b/packages/analytics/amplify_analytics_pinpoint/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_analytics_pinpoint description: The Amplify Flutter Analytics category plugin using the AWS Pinpoint provider. -version: 2.5.0 +version: 2.6.0 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/analytics/amplify_analytics_pinpoint issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -19,9 +19,9 @@ platforms: web: dependencies: - amplify_analytics_pinpoint_dart: ">=0.4.6 <0.5.0" - amplify_core: ">=2.5.0 <2.6.0" - amplify_db_common: ">=0.4.6 <0.5.0" + amplify_analytics_pinpoint_dart: ">=0.4.7 <0.5.0" + amplify_core: ">=2.6.0 <2.7.0" + amplify_db_common: ">=0.4.8 <0.5.0" amplify_secure_storage: ">=0.5.7 <0.6.0" aws_common: ">=0.7.3 <0.8.0" device_info_plus: ^10.0.1 diff --git a/packages/analytics/amplify_analytics_pinpoint_dart/CHANGELOG.md b/packages/analytics/amplify_analytics_pinpoint_dart/CHANGELOG.md index 229ad89a6c..16ad5bccc8 100644 --- a/packages/analytics/amplify_analytics_pinpoint_dart/CHANGELOG.md +++ b/packages/analytics/amplify_analytics_pinpoint_dart/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.7 + +- Minor bug fixes and improvements + ## 0.4.6 - Minor bug fixes and improvements diff --git a/packages/analytics/amplify_analytics_pinpoint_dart/pubspec.yaml b/packages/analytics/amplify_analytics_pinpoint_dart/pubspec.yaml index 5547a81898..a4e4cff45b 100644 --- a/packages/analytics/amplify_analytics_pinpoint_dart/pubspec.yaml +++ b/packages/analytics/amplify_analytics_pinpoint_dart/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_analytics_pinpoint_dart description: A Dart-only implementation of the Amplify Analytics plugin for Pinpoint. -version: 0.4.6 +version: 0.4.7 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/analytics/amplify_analytics_pinpoint_dart issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -9,7 +9,7 @@ environment: sdk: ^3.5.0 dependencies: - amplify_core: ">=2.5.0 <2.6.0" + amplify_core: ">=2.6.0 <2.7.0" amplify_db_common_dart: ">=0.4.7 <0.5.0" amplify_secure_storage_dart: ">=0.5.3 <0.6.0" aws_common: ">=0.7.3 <0.8.0" diff --git a/packages/api/amplify_api/CHANGELOG.md b/packages/api/amplify_api/CHANGELOG.md index 02d776882e..86268caaef 100644 --- a/packages/api/amplify_api/CHANGELOG.md +++ b/packages/api/amplify_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +- Minor bug fixes and improvements + ## 2.5.0 - Minor bug fixes and improvements diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml index 1b93bdaeba..3f0fd6ed9b 100644 --- a/packages/api/amplify_api/pubspec.yaml +++ b/packages/api/amplify_api/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_api description: The Amplify Flutter API category plugin, supporting GraphQL and REST operations. -version: 2.5.0 +version: 2.6.0 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/api/amplify_api issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -20,8 +20,8 @@ platforms: dependencies: amplify_api_dart: ">=0.5.7 <0.6.0" - amplify_core: ">=2.5.0 <2.6.0" - amplify_flutter: ">=2.5.0 <2.6.0" + amplify_core: ">=2.6.0 <2.7.0" + amplify_flutter: ">=2.6.0 <2.7.0" connectivity_plus: ^6.0.1 flutter: sdk: flutter diff --git a/packages/api/amplify_api_dart/CHANGELOG.md b/packages/api/amplify_api_dart/CHANGELOG.md index 1d073337b5..ab4643ee9a 100644 --- a/packages/api/amplify_api_dart/CHANGELOG.md +++ b/packages/api/amplify_api_dart/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.8 + +- Minor bug fixes and improvements + ## 0.5.7 - Minor bug fixes and improvements diff --git a/packages/api/amplify_api_dart/lib/src/graphql/factories/graphql_request_factory.dart b/packages/api/amplify_api_dart/lib/src/graphql/factories/graphql_request_factory.dart index 36ac085546..e5ce1b6ba2 100644 --- a/packages/api/amplify_api_dart/lib/src/graphql/factories/graphql_request_factory.dart +++ b/packages/api/amplify_api_dart/lib/src/graphql/factories/graphql_request_factory.dart @@ -176,11 +176,6 @@ class GraphQLRequestFactory { upperOutput = '(\$filter: ModelSubscription${modelName}FilterInput)'; lowerOutput = r'(filter: $filter)'; } - default: - throw const ApiOperationException( - 'GraphQL Request Operation is currently unsupported', - recoverySuggestion: 'please use a supported GraphQL operation', - ); } return DocumentInputs(upperOutput, lowerOutput); diff --git a/packages/api/amplify_api_dart/pubspec.yaml b/packages/api/amplify_api_dart/pubspec.yaml index 83d0042101..6484a7606c 100644 --- a/packages/api/amplify_api_dart/pubspec.yaml +++ b/packages/api/amplify_api_dart/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_api_dart description: The Amplify API category plugin in Dart-only, supporting GraphQL and REST operations. -version: 0.5.7 +version: 0.5.8 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/api/amplify_api_dart issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -9,9 +9,9 @@ environment: sdk: ^3.5.0 dependencies: - amplify_core: ">=2.5.0 <2.6.0" + amplify_core: ">=2.6.0 <2.7.0" async: ^2.10.0 - aws_common: ">=0.7.4 <0.8.0" + aws_common: ">=0.7.5 <0.8.0" collection: ^1.15.0 json_annotation: ">=4.9.0 <4.10.0" meta: ^1.7.0 diff --git a/packages/auth/amplify_auth_cognito/CHANGELOG.md b/packages/auth/amplify_auth_cognito/CHANGELOG.md index ecdb34bd2a..64f0625878 100644 --- a/packages/auth/amplify_auth_cognito/CHANGELOG.md +++ b/packages/auth/amplify_auth_cognito/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +- Minor bug fixes and improvements + ## 2.5.0 ### Features diff --git a/packages/auth/amplify_auth_cognito/pubspec.yaml b/packages/auth/amplify_auth_cognito/pubspec.yaml index d592e21bbb..525051610f 100644 --- a/packages/auth/amplify_auth_cognito/pubspec.yaml +++ b/packages/auth/amplify_auth_cognito/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_auth_cognito description: The Amplify Flutter Auth category plugin using the AWS Cognito provider. -version: 2.5.0 +version: 2.6.0 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/auth/amplify_auth_cognito issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -19,11 +19,11 @@ platforms: web: dependencies: - amplify_analytics_pinpoint: ">=2.5.0 <2.6.0" - amplify_analytics_pinpoint_dart: ">=0.4.6 <0.5.0" - amplify_auth_cognito_dart: ">=0.11.7 <0.12.0" - amplify_core: ">=2.5.0 <2.6.0" - amplify_flutter: ">=2.5.0 <2.6.0" + amplify_analytics_pinpoint: ">=2.6.0 <2.7.0" + amplify_analytics_pinpoint_dart: ">=0.4.7 <0.5.0" + amplify_auth_cognito_dart: ">=0.11.8 <0.12.0" + amplify_core: ">=2.6.0 <2.7.0" + amplify_flutter: ">=2.6.0 <2.7.0" amplify_secure_storage: ">=0.5.7 <0.6.0" async: ^2.10.0 flutter: diff --git a/packages/auth/amplify_auth_cognito_dart/CHANGELOG.md b/packages/auth/amplify_auth_cognito_dart/CHANGELOG.md index e5047e781d..e353dd4686 100644 --- a/packages/auth/amplify_auth_cognito_dart/CHANGELOG.md +++ b/packages/auth/amplify_auth_cognito_dart/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.11.8 + +- Minor bug fixes and improvements + ## 0.11.7 ### Features diff --git a/packages/auth/amplify_auth_cognito_dart/pubspec.yaml b/packages/auth/amplify_auth_cognito_dart/pubspec.yaml index 31599ee8d6..6cbd0d0f97 100644 --- a/packages/auth/amplify_auth_cognito_dart/pubspec.yaml +++ b/packages/auth/amplify_auth_cognito_dart/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_auth_cognito_dart description: A Dart-only implementation of the Amplify Auth plugin for Cognito. -version: 0.11.7 +version: 0.11.8 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/next/packages/auth/amplify_auth_cognito_dart issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -9,8 +9,8 @@ environment: sdk: ^3.5.0 dependencies: - amplify_analytics_pinpoint_dart: ">=0.4.6 <0.5.0" - amplify_core: ">=2.5.0 <2.6.0" + amplify_analytics_pinpoint_dart: ">=0.4.7 <0.5.0" + amplify_core: ">=2.6.0 <2.7.0" amplify_secure_storage_dart: ">=0.5.3 <0.6.0" async: ^2.10.0 aws_common: ">=0.7.4 <0.8.0" diff --git a/packages/authenticator/amplify_authenticator/CHANGELOG.md b/packages/authenticator/amplify_authenticator/CHANGELOG.md index eda1931983..1080ef91db 100644 --- a/packages/authenticator/amplify_authenticator/CHANGELOG.md +++ b/packages/authenticator/amplify_authenticator/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.3.2 + +- Minor bug fixes and improvements + +## 2.3.1 + +### Fixes +- fix(authenticator): recover from exceptions during reset password flow ([#5672](https://github.com/aws-amplify/amplify-flutter/pull/5672)) + ## 2.3.0 ### Features diff --git a/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart b/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart index 5604f7bd21..518957e292 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart @@ -261,8 +261,6 @@ class StateMachineBloc } } yield* _checkUserVerification(); - default: - break; } } on AuthNotAuthorizedException { /// The .failAuthentication flag available in the DefineAuthChallenge Lambda trigger diff --git a/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart b/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart index 2524d2cb35..58f6e4a3db 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart @@ -192,7 +192,6 @@ class _FormWrapperView extends StatelessAuthenticatorComponent { form, ], ); - break; } return Padding(padding: padding, child: layout); diff --git a/packages/authenticator/amplify_authenticator/lib/src/version.dart b/packages/authenticator/amplify_authenticator/lib/src/version.dart index 3692591227..d8bcd54027 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/version.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '2.3.0'; +const packageVersion = '2.3.2'; diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/sign_in_form_field.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/sign_in_form_field.dart index d65618fa04..f7f941dbcf 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/sign_in_form_field.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_fields/sign_in_form_field.dart @@ -159,8 +159,6 @@ class _SignInTextFieldState extends _SignInFormFieldState return (v) => state.username = v; case SignInField.password: return (v) => state.password = v; - default: - return super.onChanged; } } diff --git a/packages/authenticator/amplify_authenticator/pubspec.yaml b/packages/authenticator/amplify_authenticator/pubspec.yaml index bfe53ff22e..8c2f953573 100644 --- a/packages/authenticator/amplify_authenticator/pubspec.yaml +++ b/packages/authenticator/amplify_authenticator/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_authenticator description: A prebuilt Sign In and Sign Up experience for the Amplify Auth category -version: 2.3.0 +version: 2.3.2 homepage: https://ui.docs.amplify.aws/flutter/connected-components/authenticator repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/authenticator/amplify_authenticator issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -10,11 +10,11 @@ environment: flutter: ">=3.24.0" dependencies: - amplify_auth_cognito: ">=2.5.0 <2.6.0" - amplify_core: ">=2.5.0 <2.6.0" - amplify_flutter: ">=2.5.0 <2.6.0" + amplify_auth_cognito: ">=2.6.0 <2.7.0" + amplify_core: ">=2.6.0 <2.7.0" + amplify_flutter: ">=2.6.0 <2.7.0" async: ^2.10.0 - aws_common: ">=0.7.4 <0.8.0" + aws_common: ">=0.7.5 <0.8.0" collection: ^1.15.0 flutter: sdk: flutter diff --git a/packages/aws_common/CHANGELOG.md b/packages/aws_common/CHANGELOG.md index 49a0fe51e9..f31d67894b 100644 --- a/packages/aws_common/CHANGELOG.md +++ b/packages/aws_common/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.7.5 + +### Fixes +- fix(aws_common): removed JS streamed requests ([#5797](https://github.com/aws-amplify/amplify-flutter/pull/5797)) + ## 0.7.4 - Minor bug fixes and improvements diff --git a/packages/aws_common/lib/src/http/aws_http_client_js.dart b/packages/aws_common/lib/src/http/aws_http_client_js.dart index c764995b6c..1cc642e70b 100644 --- a/packages/aws_common/lib/src/http/aws_http_client_js.dart +++ b/packages/aws_common/lib/src/http/aws_http_client_js.dart @@ -71,13 +71,7 @@ class AWSHttpClientImpl extends AWSHttpClient { requestProgressController.close(); }, ).takeUntil(cancelTrigger.future); - Object body; - if (request.scheme == 'http' || - supportedProtocols.supports(AlpnProtocol.http1_1)) { - body = Uint8List.fromList(await collectBytes(stream)); - } else { - body = stream; - } + final body = Uint8List.fromList(await collectBytes(stream)); if (completer.isCanceled) return; final resp = await fetch( diff --git a/packages/aws_common/pubspec.yaml b/packages/aws_common/pubspec.yaml index ef4117323f..7c3e389187 100644 --- a/packages/aws_common/pubspec.yaml +++ b/packages/aws_common/pubspec.yaml @@ -1,6 +1,6 @@ name: aws_common description: Common types and utilities used across AWS and Amplify packages. -version: 0.7.4 +version: 0.7.5 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/aws_common issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues diff --git a/packages/common/amplify_db_common/CHANGELOG.md b/packages/common/amplify_db_common/CHANGELOG.md index 058a24c256..7b64c012bf 100644 --- a/packages/common/amplify_db_common/CHANGELOG.md +++ b/packages/common/amplify_db_common/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.4.8 + +- Minor bug fixes and improvements + +## 0.4.7 + +### Fixes +- fix(common): added a flag to opt out of bundling sqlite3 for windows apps ([#5680](https://github.com/aws-amplify/amplify-flutter/pull/5680)) + ## 0.4.6 ### Fixes diff --git a/packages/common/amplify_db_common/pubspec.yaml b/packages/common/amplify_db_common/pubspec.yaml index 65a5d47065..7b362b411b 100644 --- a/packages/common/amplify_db_common/pubspec.yaml +++ b/packages/common/amplify_db_common/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_db_common description: Common utilities for working with databases such as SQLite. -version: 0.4.6 +version: 0.4.8 homepage: https://github.com/aws-amplify/amplify-flutter/tree/main repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/common/amplify_db_common issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -10,7 +10,7 @@ environment: flutter: ">=3.24.0" dependencies: - amplify_db_common_dart: ">=0.4.7 <0.5.0" + amplify_db_common_dart: ">=0.4.8 <0.5.0" drift: ">=2.22.0 <2.23.0" flutter: sdk: flutter diff --git a/packages/common/amplify_db_common_dart/CHANGELOG.md b/packages/common/amplify_db_common_dart/CHANGELOG.md index 27df41af81..909775f894 100644 --- a/packages/common/amplify_db_common_dart/CHANGELOG.md +++ b/packages/common/amplify_db_common_dart/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.8 + +- Minor bug fixes and improvements + ## 0.4.7 ### Fixes diff --git a/packages/common/amplify_db_common_dart/pubspec.yaml b/packages/common/amplify_db_common_dart/pubspec.yaml index 0e9e660bf3..4bc186ee56 100644 --- a/packages/common/amplify_db_common_dart/pubspec.yaml +++ b/packages/common/amplify_db_common_dart/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_db_common_dart description: Common utilities for working with databases such as sqlite. Used throughout Amplify packages. -version: 0.4.7 +version: 0.4.8 homepage: https://github.com/aws-amplify/amplify-flutter/tree/main repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/common/amplify_db_common_dart issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -9,7 +9,7 @@ environment: sdk: ^3.5.0 dependencies: - amplify_core: ">=2.5.0 <2.6.0" + amplify_core: ">=2.6.0 <2.7.0" async: ^2.10.0 aws_common: ">=0.7.3 <0.8.0" drift: ">=2.22.0 <2.23.0" diff --git a/packages/notifications/push/amplify_push_notifications/CHANGELOG.md b/packages/notifications/push/amplify_push_notifications/CHANGELOG.md index 6949519835..b7b4172788 100644 --- a/packages/notifications/push/amplify_push_notifications/CHANGELOG.md +++ b/packages/notifications/push/amplify_push_notifications/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +- Minor bug fixes and improvements + ## 2.5.0 - Minor bug fixes and improvements diff --git a/packages/notifications/push/amplify_push_notifications/pubspec.yaml b/packages/notifications/push/amplify_push_notifications/pubspec.yaml index c52ee74aab..6a24b700ed 100644 --- a/packages/notifications/push/amplify_push_notifications/pubspec.yaml +++ b/packages/notifications/push/amplify_push_notifications/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_push_notifications description: The Amplify Flutter Push Notifications package implementing features agnostic of an AWS Service such as Pinpoint. -version: 2.5.0 +version: 2.6.0 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -9,7 +9,7 @@ environment: flutter: ">=3.24.0" dependencies: - amplify_core: ">=2.5.0 <2.6.0" + amplify_core: ">=2.6.0 <2.7.0" amplify_secure_storage: ">=0.5.5 <0.6.0" async: ^2.10.0 flutter: diff --git a/packages/notifications/push/amplify_push_notifications_pinpoint/CHANGELOG.md b/packages/notifications/push/amplify_push_notifications_pinpoint/CHANGELOG.md index 969683cc11..dc3d3a24c1 100644 --- a/packages/notifications/push/amplify_push_notifications_pinpoint/CHANGELOG.md +++ b/packages/notifications/push/amplify_push_notifications_pinpoint/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +- Minor bug fixes and improvements + ## 2.5.0 - Minor bug fixes and improvements diff --git a/packages/notifications/push/amplify_push_notifications_pinpoint/pubspec.yaml b/packages/notifications/push/amplify_push_notifications_pinpoint/pubspec.yaml index 318fe21eeb..5d5b2e1336 100644 --- a/packages/notifications/push/amplify_push_notifications_pinpoint/pubspec.yaml +++ b/packages/notifications/push/amplify_push_notifications_pinpoint/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_push_notifications_pinpoint description: The Amplify Flutter Push Notifications category plugin using the AWS Pinpoint provider. -version: 2.5.0 +version: 2.6.0 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -14,12 +14,12 @@ platforms: android: dependencies: - amplify_analytics_pinpoint: ">=2.5.0 <2.6.0" - amplify_analytics_pinpoint_dart: ">=0.4.6 <0.5.0" - amplify_auth_cognito: ">=2.5.0 <2.6.0" - amplify_core: ">=2.5.0 <2.6.0" - amplify_flutter: ">=2.5.0 <2.6.0" - amplify_push_notifications: ">=2.5.0 <2.6.0" + amplify_analytics_pinpoint: ">=2.6.0 <2.7.0" + amplify_analytics_pinpoint_dart: ">=0.4.7 <0.5.0" + amplify_auth_cognito: ">=2.6.0 <2.7.0" + amplify_core: ">=2.6.0 <2.7.0" + amplify_flutter: ">=2.6.0 <2.7.0" + amplify_push_notifications: ">=2.6.0 <2.7.0" amplify_secure_storage: ">=0.5.5 <0.6.0" flutter: sdk: flutter diff --git a/packages/storage/amplify_storage_s3/CHANGELOG.md b/packages/storage/amplify_storage_s3/CHANGELOG.md index b76ffff04d..4b343e27e8 100644 --- a/packages/storage/amplify_storage_s3/CHANGELOG.md +++ b/packages/storage/amplify_storage_s3/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.6.0 + +### Features +- feat(storage): add multi-bucket feature support ([#5681](https://github.com/aws-amplify/amplify-flutter/pull/5681)) + +## 2.5.1 + +- Minor bug fixes and improvements + ## 2.5.0 - Minor bug fixes and improvements diff --git a/packages/storage/amplify_storage_s3/example/integration_test/copy_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/copy_test.dart index 87afda83b3..12e892ea06 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/copy_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/copy_test.dart @@ -138,5 +138,90 @@ void main() { expect(result.copiedItem.path, destinationPath); }); }); + + group('multi bucket', () { + final data = 'copy data'.codeUnits; + final bucket1 = StorageBucket.fromOutputs( + 'Storage Integ Test main bucket', + ); + final bucket2 = StorageBucket.fromOutputs( + 'Storage Integ Test secondary bucket', + ); + final bucket1PathSource = 'public/multi-bucket-get-url-${uuid()}'; + final bucket2PathSource = 'public/multi-bucket-get-url-${uuid()}'; + final bucket2PathDestination = 'public/multi-bucket-get-url-${uuid()}'; + final storageBucket1PathSource = + StoragePath.fromString(bucket1PathSource); + final storageBucket2PathSource = + StoragePath.fromString(bucket2PathSource); + final storageBucket2PathDestination = + StoragePath.fromString(bucket2PathDestination); + + setUp(() async { + await configure(amplifyEnvironments['main']!); + addTearDownPath(storageBucket1PathSource); + addTearDownPath(storageBucket2PathSource); + addTearDownPath(storageBucket2PathDestination); + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: storageBucket1PathSource, + options: StorageUploadDataOptions( + bucket: bucket1, + ), + ).result; + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: storageBucket2PathSource, + options: StorageUploadDataOptions( + bucket: bucket2, + ), + ).result; + }); + + testWidgets('copy to a different bucket', (_) async { + final result = await Amplify.Storage.copy( + source: storageBucket1PathSource, + destination: storageBucket2PathDestination, + options: StorageCopyOptions( + buckets: CopyBuckets( + source: bucket1, + destination: bucket2, + ), + ), + ).result; + expect(result.copiedItem.path, bucket2PathDestination); + + final downloadResult = await Amplify.Storage.downloadData( + path: storageBucket2PathDestination, + options: StorageDownloadDataOptions(bucket: bucket2), + ).result; + expect( + downloadResult.bytes, + data, + ); + }); + + testWidgets('copy to the same bucket', (_) async { + final result = await Amplify.Storage.copy( + source: storageBucket2PathSource, + destination: storageBucket2PathDestination, + options: StorageCopyOptions( + buckets: CopyBuckets.sameBucket( + bucket2, + ), + ), + ).result; + expect(result.copiedItem.path, bucket2PathDestination); + + final downloadResult = await Amplify.Storage.downloadData( + path: storageBucket2PathDestination, + options: StorageDownloadDataOptions(bucket: bucket2), + ).result; + expect( + downloadResult.bytes, + data, + ); + }); + }); }); } diff --git a/packages/storage/amplify_storage_s3/example/integration_test/download_data_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/download_data_test.dart index eb028d3919..bf5f7b96f0 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/download_data_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/download_data_test.dart @@ -132,6 +132,47 @@ void main() { expect(utf8.decode(downloadResult.bytes), 'data'); expect(downloadResult.downloadedItem.path, publicPath); }); + + testWidgets('multi bucket', (_) async { + final mainBucket = + StorageBucket.fromOutputs('Storage Integ Test main bucket'); + final secondaryBucket = StorageBucket.fromOutputs( + 'Storage Integ Test secondary bucket', + ); + await Amplify.Storage.uploadData( + path: StoragePath.fromString(publicPath), + data: StorageDataPayload.bytes(bytesData), + options: StorageUploadDataOptions( + bucket: secondaryBucket, + ), + ).result; + + final downloadResult = await Amplify.Storage.downloadData( + path: StoragePath.fromString(publicPath), + options: StorageDownloadDataOptions(bucket: mainBucket), + ).result; + expect( + downloadResult.bytes, + bytesData, + ); + expect( + downloadResult.downloadedItem.path, + publicPath, + ); + + final downloadSecondaryResult = await Amplify.Storage.downloadData( + path: StoragePath.fromString(publicPath), + options: StorageDownloadDataOptions(bucket: secondaryBucket), + ).result; + expect( + downloadSecondaryResult.bytes, + bytesData, + ); + expect( + downloadSecondaryResult.downloadedItem.path, + publicPath, + ); + }); }); group('download progress', () { diff --git a/packages/storage/amplify_storage_s3/example/integration_test/download_file_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/download_file_test.dart index fb3dd7e554..4ed64792e5 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/download_file_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/download_file_test.dart @@ -28,7 +28,8 @@ void main() { final name = 'download-file-with-identity-id-${uuid()}'; final metadataFilePath = 'public/download-file-get-properties-${uuid()}'; final metadata = {'description': 'foo'}; - + final secondaryBucket = + StorageBucket.fromOutputs('Storage Integ Test secondary bucket'); setUpAll(() async { directory = kIsWeb ? '/' : (await getTemporaryDirectory()).path; }); @@ -51,6 +52,22 @@ void main() { ), ).result; + // secondary bucket uploads + + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: StoragePath.fromString(publicPath), + options: StorageUploadDataOptions(bucket: secondaryBucket), + ).result; + + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: StoragePath.fromIdentityId( + (identityId) => 'private/$identityId/$name', + ), + options: StorageUploadDataOptions(bucket: secondaryBucket), + ).result; + await Amplify.Storage.uploadData( data: StorageDataPayload.bytes(data), path: StoragePath.fromString(metadataFilePath), @@ -68,6 +85,48 @@ void main() { ); }); + group('multibucket', () { + testWidgets('to file', (_) async { + final downloadFilePath = '$directory/downloaded-file.txt'; + + final result = await Amplify.Storage.downloadFile( + path: StoragePath.fromString(publicPath), + localFile: AWSFile.fromPath(downloadFilePath), + options: StorageDownloadFileOptions( + bucket: secondaryBucket, + ), + ).result; + + // Web browsers do not grant access to read arbitrary files + if (!kIsWeb) { + final downloadedFile = await readFile(path: downloadFilePath); + expect(downloadedFile, data); + } + + expect(result.localFile.path, downloadFilePath); + expect(result.downloadedItem.path, publicPath); + }); + testWidgets('from identity ID', (_) async { + final downloadFilePath = '$directory/downloaded-file.txt'; + final result = await Amplify.Storage.downloadFile( + path: StoragePath.fromIdentityId( + (identityId) => 'private/$identityId/$name', + ), + localFile: AWSFile.fromPath(downloadFilePath), + options: StorageDownloadFileOptions( + bucket: secondaryBucket, + ), + ).result; + + if (!kIsWeb) { + final downloadedFile = await readFile(path: downloadFilePath); + expect(downloadedFile, data); + } + expect(result.localFile.path, downloadFilePath); + expect(result.downloadedItem.path, identityPath); + }); + }); + group('for file type', () { testWidgets('to file', (_) async { final downloadFilePath = '$directory/downloaded-file.txt'; diff --git a/packages/storage/amplify_storage_s3/example/integration_test/get_properties_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/get_properties_test.dart index 9540e49650..5bdf3170b0 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/get_properties_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/get_properties_test.dart @@ -101,5 +101,125 @@ void main() { expect(result.storageItem.size, data.length); }); }); + group('multibucket config', () { + final mainBucket = + StorageBucket.fromOutputs('Storage Integ Test main bucket'); + final secondaryBucket = + StorageBucket.fromOutputs('Storage Integ Test secondary bucket'); + setUpAll(() async { + await configure(amplifyEnvironments['main']!); + addTearDownPath(StoragePath.fromString(path)); + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: StoragePath.fromString(path), + options: StorageUploadDataOptions( + metadata: metadata, + bucket: mainBucket, + ), + ).result; + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: StoragePath.fromString(path), + options: StorageUploadDataOptions( + metadata: metadata, + bucket: secondaryBucket, + ), + ).result; + }); + + testWidgets('String StoragePath', (_) async { + final result = await Amplify.Storage.getProperties( + path: StoragePath.fromString(path), + options: StorageGetPropertiesOptions( + bucket: mainBucket, + ), + ).result; + expect(result.storageItem.path, path); + expect(result.storageItem.metadata, metadata); + expect(result.storageItem.eTag, isNotNull); + expect(result.storageItem.size, data.length); + + final resultSecondaryBucket = await Amplify.Storage.getProperties( + path: StoragePath.fromString(path), + options: StorageGetPropertiesOptions( + bucket: secondaryBucket, + ), + ).result; + expect(resultSecondaryBucket.storageItem.path, path); + expect(resultSecondaryBucket.storageItem.metadata, metadata); + expect(resultSecondaryBucket.storageItem.eTag, isNotNull); + expect(resultSecondaryBucket.storageItem.size, data.length); + }); + + testWidgets('with identity ID', (_) async { + final userIdentityId = await signInNewUser(); + final name = 'get-properties-with-identity-id-${uuid()}'; + final data = 'with identity ID'.codeUnits; + final expectedResolvedPath = 'private/$userIdentityId/$name'; + addTearDownPath(StoragePath.fromString(expectedResolvedPath)); + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: StoragePath.fromString(expectedResolvedPath), + options: StorageUploadDataOptions( + metadata: metadata, + bucket: secondaryBucket, + ), + ).result; + final result = await Amplify.Storage.getProperties( + path: StoragePath.fromIdentityId( + ((identityId) => 'private/$identityId/$name'), + ), + options: StorageGetPropertiesOptions( + bucket: secondaryBucket, + ), + ).result; + expect(result.storageItem.path, expectedResolvedPath); + expect(result.storageItem.metadata, metadata); + expect(result.storageItem.eTag, isNotNull); + expect(result.storageItem.size, data.length); + }); + + testWidgets('not existent path', (_) async { + // we expect StorageNotFoundException here since there is no data uploaded to either bucket on this path + await expectLater( + () => Amplify.Storage.getProperties( + path: const StoragePath.fromString('public/not-existent-path'), + options: StorageGetPropertiesOptions( + bucket: mainBucket, + ), + ).result, + throwsA(isA()), + ); + await expectLater( + () => Amplify.Storage.getProperties( + path: const StoragePath.fromString('public/not-existent-path'), + options: StorageGetPropertiesOptions( + bucket: secondaryBucket, + ), + ).result, + throwsA(isA()), + ); + }); + testWidgets('unauthorized path', (_) async { + await expectLater( + () => Amplify.Storage.getProperties( + path: const StoragePath.fromString('unauthorized/path'), + options: StorageGetPropertiesOptions( + bucket: mainBucket, + ), + ).result, + throwsA(isA()), + ); + await expectLater( + () => Amplify.Storage.getProperties( + path: const StoragePath.fromString('unauthorized/path'), + options: StorageGetPropertiesOptions( + bucket: secondaryBucket, + ), + ).result, + throwsA(isA()), + ); + }); + }); }); } diff --git a/packages/storage/amplify_storage_s3/example/integration_test/get_url_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/get_url_test.dart index 2c26f5b610..4d49674900 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/get_url_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/get_url_test.dart @@ -172,6 +172,62 @@ void main() { expect(actualData, data); }); }); + + group('multi bucket', () { + final mainBucket = StorageBucket.fromOutputs( + 'Storage Integ Test main bucket', + ); + final secondaryBucket = StorageBucket.fromOutputs( + 'Storage Integ Test secondary bucket', + ); + final pathMain = 'public/multi-bucket-get-url-${uuid()}'; + final pathSecondary = 'public/multi-bucket-get-url-${uuid()}'; + final storagePathMain = StoragePath.fromString(pathMain); + final storagePathSecondary = StoragePath.fromString(pathSecondary); + + setUp(() async { + addTearDownPath(storagePathMain); + addTearDownPath(storagePathSecondary); + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: storagePathMain, + options: StorageUploadDataOptions( + bucket: mainBucket, + ), + ).result; + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: storagePathSecondary, + options: StorageUploadDataOptions( + bucket: secondaryBucket, + ), + ).result; + }); + + testWidgets('can get url from main bucket', (_) async { + final result = await Amplify.Storage.getUrl( + path: storagePathMain, + options: StorageGetUrlOptions( + bucket: mainBucket, + ), + ).result; + expect(result.url.path, '/$pathMain'); + final actualData = await readData(result.url); + expect(actualData, data); + }); + + testWidgets('can get url from secondary bucket', (_) async { + final result = await Amplify.Storage.getUrl( + path: storagePathSecondary, + options: StorageGetUrlOptions( + bucket: secondaryBucket, + ), + ).result; + expect(result.url.path, '/$pathSecondary'); + final actualData = await readData(result.url); + expect(actualData, data); + }); + }); }); group('config with dots in name', () { 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 22e2a2766b..15c3737225 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 @@ -20,18 +20,41 @@ void main() { '$uniquePrefix/file2.txt', '$uniquePrefix/subdir/file3.txt', '$uniquePrefix/subdir2#file4.txt', + '$uniquePrefix/file5.txt', + '$uniquePrefix/file6.txt', + '$uniquePrefix/subdir3/file7.txt', + '$uniquePrefix/subdir4#file8.txt', ]; group('standard config', () { + final mainBucket = + StorageBucket.fromOutputs('Storage Integ Test main bucket'); + final secondaryBucket = StorageBucket.fromOutputs( + 'Storage Integ Test secondary bucket', + ); setUpAll(() async { await configure(amplifyEnvironments['main']!); - - for (final path in uploadedPaths) { + for (var pathIndex = 0; + pathIndex < uploadedPaths.length ~/ 2; + pathIndex++) { await Amplify.Storage.uploadData( - path: StoragePath.fromString(path), + path: StoragePath.fromString(uploadedPaths[pathIndex]), data: StorageDataPayload.bytes('test content'.codeUnits), + options: StorageUploadDataOptions( + bucket: mainBucket, + ), + ).result; + } + for (var pathIndex = uploadedPaths.length ~/ 2; + pathIndex < uploadedPaths.length; + pathIndex++) { + await Amplify.Storage.uploadData( + path: StoragePath.fromString(uploadedPaths[pathIndex]), + data: StorageDataPayload.bytes('test content'.codeUnits), + options: StorageUploadDataOptions( + bucket: secondaryBucket, + ), ).result; } - for (final path in uploadedPaths) { addTearDownPath(StoragePath.fromString(path)); } @@ -39,13 +62,31 @@ void main() { group('list() without options', () { testWidgets('should list all files with unique prefix', (_) async { - final listResult = await Amplify.Storage.list( + // this will use the main bucket by default when no optional bucket is specified + final listResultMainBucket = await Amplify.Storage.list( path: StoragePath.fromString(uniquePrefix), ).result; - - for (final uploadedPath in uploadedPaths) { + final listResultSecondaryBucket = await Amplify.Storage.list( + path: StoragePath.fromString(uniquePrefix), + options: StorageListOptions( + bucket: secondaryBucket, + ), + ).result; + for (var pathIndex = 0; + pathIndex < uploadedPaths.length ~/ 2; + pathIndex++) { + expect( + listResultMainBucket.items + .any((item) => item.path == uploadedPaths[pathIndex]), + isTrue, + ); + } + for (var pathIndex = uploadedPaths.length ~/ 2; + pathIndex < uploadedPaths.length; + pathIndex++) { expect( - listResult.items.any((item) => item.path == uploadedPath), + listResultSecondaryBucket.items + .any((item) => item.path == uploadedPaths[pathIndex]), isTrue, ); } @@ -101,6 +142,17 @@ void main() { ), ).result as S3ListResult; + final listResultSecondaryBucket = await Amplify.Storage.list( + path: StoragePath.fromString('$uniquePrefix/'), + options: StorageListOptions( + pluginOptions: const S3ListPluginOptions( + excludeSubPaths: true, + delimiter: '#', + ), + bucket: secondaryBucket, + ), + ).result as S3ListResult; + expect(listResult.items.length, 3); expect(listResult.items.first.path, contains('file1.txt')); @@ -110,6 +162,19 @@ void main() { '$uniquePrefix/subdir2#', ); expect(listResult.metadata.delimiter, '#'); + + expect(listResultSecondaryBucket.items.length, 3); + expect( + listResultSecondaryBucket.items.first.path, + contains('file5.txt'), + ); + + expect(listResultSecondaryBucket.metadata.subPaths.length, 1); + expect( + listResultSecondaryBucket.metadata.subPaths.first, + '$uniquePrefix/subdir4#', + ); + expect(listResultSecondaryBucket.metadata.delimiter, '#'); }); }); @@ -123,6 +188,20 @@ void main() { expect(listResult.items.length, 2); expect(listResult.items.first.path, contains('file1.txt')); + + final listResultSecondaryBucket = await Amplify.Storage.list( + path: StoragePath.fromString(uniquePrefix), + options: StorageListOptions( + pageSize: 2, + bucket: secondaryBucket, + ), + ).result; + + expect(listResultSecondaryBucket.items.length, 2); + expect( + listResultSecondaryBucket.items.first.path, + contains('file5.txt'), + ); }); testWidgets('should list files with pagination', (_) async { @@ -157,8 +236,22 @@ void main() { ), ).result; - expect(listResult.items.length, uploadedPaths.length); + expect(listResult.items.length, uploadedPaths.length ~/ 2); expect(listResult.nextToken, isNull); + + final listResultSecondaryBucket = await Amplify.Storage.list( + path: StoragePath.fromString(uniquePrefix), + options: StorageListOptions( + pluginOptions: const S3ListPluginOptions.listAll(), + bucket: secondaryBucket, + ), + ).result; + + expect( + listResultSecondaryBucket.items.length, + uploadedPaths.length ~/ 2, + ); + expect(listResultSecondaryBucket.nextToken, isNull); }); }); }); diff --git a/packages/storage/amplify_storage_s3/example/integration_test/remove_many_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/remove_many_test.dart index 87c5ff2e3b..435ed1d986 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/remove_many_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/remove_many_test.dart @@ -90,6 +90,127 @@ void main() { }); }); + group('Multi-bucket', () { + final mainBucket = StorageBucket.fromOutputs( + 'Storage Integ Test main bucket', + ); + final secondaryBucket = StorageBucket.fromOutputs( + 'Storage Integ Test secondary bucket', + ); + final path1 = 'public/multi-bucket-remove-many-${uuid()}'; + final path2 = 'public/multi-bucket-remove-many-${uuid()}'; + final storagePath1 = StoragePath.fromString(path1); + final storagePath2 = StoragePath.fromString(path2); + setUp(() async { + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes('data'.codeUnits), + path: storagePath1, + options: StorageUploadDataOptions( + bucket: mainBucket, + ), + ).result; + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes('data'.codeUnits), + path: storagePath2, + options: StorageUploadDataOptions( + bucket: mainBucket, + ), + ).result; + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes('data'.codeUnits), + path: storagePath1, + options: StorageUploadDataOptions( + bucket: secondaryBucket, + ), + ).result; + await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes('data'.codeUnits), + path: storagePath2, + options: StorageUploadDataOptions( + bucket: secondaryBucket, + ), + ).result; + }); + + testWidgets('removes objects from main bucket', (_) async { + expect( + await objectExists( + storagePath1, + bucket: mainBucket, + ), + true, + ); + expect( + await objectExists( + storagePath2, + bucket: mainBucket, + ), + true, + ); + final result = await Amplify.Storage.removeMany( + paths: [storagePath1, storagePath2], + options: StorageRemoveManyOptions( + bucket: mainBucket, + ), + ).result; + expect( + await objectExists( + storagePath1, + bucket: mainBucket, + ), + false, + ); + expect( + await objectExists( + storagePath2, + bucket: mainBucket, + ), + false, + ); + final removedPaths = result.removedItems.map((i) => i.path).toList(); + expect(removedPaths, unorderedEquals([path1, path2])); + }); + + testWidgets('removes objects from secondary bucket', (_) async { + expect( + await objectExists( + storagePath1, + bucket: secondaryBucket, + ), + true, + ); + expect( + await objectExists( + storagePath2, + bucket: secondaryBucket, + ), + true, + ); + final result = await Amplify.Storage.removeMany( + paths: [storagePath1, storagePath2], + options: StorageRemoveManyOptions( + bucket: secondaryBucket, + ), + ).result; + expect( + await objectExists( + storagePath1, + bucket: secondaryBucket, + ), + false, + ); + expect( + await objectExists( + storagePath2, + bucket: secondaryBucket, + ), + false, + ); + final removedPaths = result.removedItems.map((i) => i.path).toList(); + expect(removedPaths, unorderedEquals([path1, path2])); + }); + }); + testWidgets('unauthorized path', (_) async { final result = await Amplify.Storage.removeMany( paths: [const StoragePath.fromString('unauthorized/path')], 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..c54822a190 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,120 @@ 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, + options: StorageUploadDataOptions( + 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, + options: StorageUploadDataOptions( + 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/upload_data_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/upload_data_test.dart index 89880ae4ef..68fd42aa35 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/upload_data_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/upload_data_test.dart @@ -252,6 +252,59 @@ void main() { }); }); + group('multi-bucket', () { + final mainBucket = + StorageBucket.fromOutputs('Storage Integ Test main bucket'); + final secondaryBucket = StorageBucket.fromOutputs( + 'Storage Integ Test secondary bucket', + ); + + testWidgets('uploads to multiple buckets', (_) async { + final path = 'public/multi-bucket-upload-data-${uuid()}'; + final storagePath = StoragePath.fromString(path); + final data = 'multi bucket upload data byte'.codeUnits; + addTearDownMultiBucket( + storagePath, + [mainBucket, secondaryBucket], + ); + // main bucket + final mainResult = await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: storagePath, + options: StorageUploadDataOptions( + bucket: mainBucket, + ), + ).result; + expect(mainResult.uploadedItem.path, path); + + final downloadMainResult = await Amplify.Storage.downloadData( + path: storagePath, + options: StorageDownloadDataOptions( + bucket: mainBucket, + ), + ).result; + expect(downloadMainResult.bytes, data); + + // secondary bucket + final secondaryResult = await Amplify.Storage.uploadData( + data: StorageDataPayload.bytes(data), + path: storagePath, + options: StorageUploadDataOptions( + bucket: secondaryBucket, + ), + ).result; + expect(secondaryResult.uploadedItem.path, path); + + final downloadSecondaryResult = await Amplify.Storage.downloadData( + path: storagePath, + options: StorageDownloadDataOptions( + bucket: secondaryBucket, + ), + ).result; + expect(downloadSecondaryResult.bytes, data); + }); + }); + group('upload progress', () { testWidgets('reports progress for byte data', (_) async { final path = 'public/upload-data-progress-bytes-${uuid()}'; diff --git a/packages/storage/amplify_storage_s3/example/integration_test/upload_file_test.dart b/packages/storage/amplify_storage_s3/example/integration_test/upload_file_test.dart index 6e1eb0581e..9113a3c4c3 100644 --- a/packages/storage/amplify_storage_s3/example/integration_test/upload_file_test.dart +++ b/packages/storage/amplify_storage_s3/example/integration_test/upload_file_test.dart @@ -11,6 +11,7 @@ import 'package:integration_test/integration_test.dart'; import 'utils/configure.dart'; import 'utils/create_file/create_file.dart'; +import 'utils/object_exists.dart'; import 'utils/sign_in_new_user.dart'; import 'utils/tear_down.dart'; @@ -220,6 +221,83 @@ void main() { }); }); + group('multi-bucket', () { + final mainBucket = + StorageBucket.fromOutputs('Storage Integ Test main bucket'); + final secondaryBucket = StorageBucket.fromOutputs( + 'Storage Integ Test secondary bucket', + ); + + testWidgets('uploads to multiple buckets', (_) async { + final fileId = uuid(); + final path = 'public/multi-bucket-upload-file-$fileId'; + final storagePath = StoragePath.fromString(path); + const content = 'upload file'; + final data = content.codeUnits; + final filePath = await createFile(path: fileId, content: content); + addTearDownMultiBucket( + storagePath, + [mainBucket, secondaryBucket], + ); + // main bucket + final mainResult = await Amplify.Storage.uploadFile( + localFile: AWSFile.fromPath(filePath), + path: storagePath, + options: StorageUploadFileOptions( + pluginOptions: const S3UploadFilePluginOptions( + useAccelerateEndpoint: true, + ), + bucket: mainBucket, + ), + ).result; + expect(mainResult.uploadedItem.path, path); + + final downloadMainResult = await Amplify.Storage.downloadData( + path: storagePath, + options: StorageDownloadDataOptions( + bucket: mainBucket, + ), + ).result; + expect(downloadMainResult.bytes, data); + + // secondary bucket + final secondaryResult = await Amplify.Storage.uploadFile( + localFile: AWSFile.fromPath(filePath), + path: storagePath, + options: StorageUploadFileOptions( + pluginOptions: const S3UploadFilePluginOptions( + useAccelerateEndpoint: true, + ), + bucket: secondaryBucket, + ), + ).result; + expect(secondaryResult.uploadedItem.path, path); + + final downloadSecondaryResult = await Amplify.Storage.downloadData( + path: storagePath, + options: StorageDownloadDataOptions( + bucket: secondaryBucket, + ), + ).result; + expect(downloadSecondaryResult.bytes, data); + + expect( + await objectExists( + storagePath, + bucket: mainBucket, + ), + true, + ); + expect( + await objectExists( + storagePath, + bucket: secondaryBucket, + ), + true, + ); + }); + }); + group('upload progress', () { testWidgets('reports progress', (_) async { final fileId = uuid(); 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..21cddc0675 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 @@ -7,11 +7,14 @@ import 'package:flutter_test/flutter_test.dart'; final _logger = AmplifyLogger().createChild('StorageTests'); /// Adds a tear down to remove the object at [path]. -void addTearDownPath(StoragePath path) { +void addTearDownPath(StoragePath path, {StorageBucket? bucket}) { addTearDown( () { try { - return Amplify.Storage.remove(path: path).result; + return Amplify.Storage.remove( + path: path, + options: StorageRemoveOptions(bucket: bucket), + ).result; } on Exception catch (e) { _logger.warn('Failed to remove file after test', e); rethrow; @@ -36,6 +39,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/pubspec.yaml b/packages/storage/amplify_storage_s3/pubspec.yaml index 836aa4c9cc..321767c9ab 100644 --- a/packages/storage/amplify_storage_s3/pubspec.yaml +++ b/packages/storage/amplify_storage_s3/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_storage_s3 description: The Amplify Flutter Storage category plugin using the AWS S3 provider. -version: 2.5.0 +version: 2.6.0 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/storage/amplify_storage_s3 issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -19,9 +19,9 @@ platforms: web: dependencies: - amplify_core: ">=2.5.0 <2.6.0" - amplify_db_common: ">=0.4.6 <0.5.0" - amplify_storage_s3_dart: ">=0.4.6 <0.5.0" + amplify_core: ">=2.6.0 <2.7.0" + amplify_db_common: ">=0.4.8 <0.5.0" + amplify_storage_s3_dart: ">=0.4.8 <0.5.0" aws_common: ">=0.7.3 <0.8.0" flutter: sdk: flutter diff --git a/packages/storage/amplify_storage_s3_dart/CHANGELOG.md b/packages/storage/amplify_storage_s3_dart/CHANGELOG.md index 9ddc326400..688a017b7a 100644 --- a/packages/storage/amplify_storage_s3_dart/CHANGELOG.md +++ b/packages/storage/amplify_storage_s3_dart/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.4.8 + +### Features +- feat(storage): add multi-bucket feature support ([#5681](https://github.com/aws-amplify/amplify-flutter/pull/5681)) + +## 0.4.7 + +### Fixes +- fix(storage): Only allow 1 batch to run at a time ([#5704](https://github.com/aws-amplify/amplify-flutter/pull/5704)) + ## 0.4.6 - Minor bug fixes and improvements 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 cc709c5802..11a2c9bc9c 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 @@ -136,6 +136,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface final s3Options = StorageListOptions( pluginOptions: s3PluginOptions, nextToken: options?.nextToken, + bucket: options?.bucket, pageSize: options?.pageSize ?? 1000, ); @@ -163,6 +164,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface final s3Options = StorageGetPropertiesOptions( pluginOptions: s3PluginOptions, + bucket: options?.bucket, ); return S3GetPropertiesOperation( @@ -189,6 +191,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface final s3Options = StorageGetUrlOptions( pluginOptions: s3PluginOptions, + bucket: options?.bucket, ); return S3GetUrlOperation( @@ -216,6 +219,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface final s3Options = StorageDownloadDataOptions( pluginOptions: s3PluginOptions, + bucket: options?.bucket, ); final bytes = BytesBuilder(); @@ -256,6 +260,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface ); options = StorageDownloadFileOptions( pluginOptions: s3PluginOptions, + bucket: options?.bucket, ); return download_file_impl.downloadFile( path: path, @@ -282,6 +287,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface final s3Options = StorageUploadDataOptions( metadata: options?.metadata ?? const {}, + bucket: options?.bucket, pluginOptions: s3PluginOptions, ); @@ -320,6 +326,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface final s3Options = StorageUploadFileOptions( metadata: options?.metadata ?? const {}, pluginOptions: s3PluginOptions, + bucket: options?.bucket, ); final uploadTask = storageS3Service.uploadFile( @@ -357,6 +364,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface final s3Options = StorageCopyOptions( pluginOptions: s3PluginOptions, + buckets: options?.buckets, ); return S3CopyOperation( @@ -385,6 +393,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface final s3Options = StorageRemoveOptions( pluginOptions: s3PluginOptions, + bucket: options?.bucket, ); return S3RemoveOperation( @@ -411,6 +420,7 @@ class AmplifyStorageS3Dart extends StoragePluginInterface final s3Options = StorageRemoveManyOptions( pluginOptions: s3PluginOptions, + bucket: options?.bucket, ); return S3RemoveManyOperation( diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/platform_impl/download_file/download_file_html.dart b/packages/storage/amplify_storage_s3_dart/lib/src/platform_impl/download_file/download_file_html.dart index 32f79a9d38..a77ee6f8c5 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/platform_impl/download_file/download_file_html.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/platform_impl/download_file/download_file_html.dart @@ -58,7 +58,7 @@ Future _downloadFromUrl({ // operation. final downloadedItem = (await storageS3Service.getProperties( path: path, - options: const StorageGetPropertiesOptions(), + options: StorageGetPropertiesOptions(bucket: options.bucket), )) .storageItem; @@ -71,6 +71,7 @@ Future _downloadFromUrl({ pluginOptions: S3GetUrlPluginOptions( useAccelerateEndpoint: s3PluginOptions.useAccelerateEndpoint, ), + bucket: options.bucket, ), )) .url; diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/platform_impl/download_file/download_file_io.dart b/packages/storage/amplify_storage_s3_dart/lib/src/platform_impl/download_file/download_file_io.dart index 5651870acd..c17e19b1db 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/platform_impl/download_file/download_file_io.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/platform_impl/download_file/download_file_io.dart @@ -32,6 +32,7 @@ S3DownloadFileOperation downloadFile({ getProperties: s3PluginOptions.getProperties, useAccelerateEndpoint: s3PluginOptions.useAccelerateEndpoint, ), + bucket: options.bucket, ); final downloadDataTask = storageS3Service.downloadData( diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/s3_client_info.dart b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/s3_client_info.dart new file mode 100644 index 0000000000..d70adc524d --- /dev/null +++ b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/s3_client_info.dart @@ -0,0 +1,19 @@ +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 { + const S3ClientInfo({ + required this.client, + required this.config, + required this.bucketName, + required this.awsRegion, + }); + + final S3Client client; + final S3ClientConfig config; + final String bucketName; + final String awsRegion; +} 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 f749de67f3..d3d8939f39 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 @@ -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; @@ -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(); @@ -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 _s3ClientsInfo = {}; transfer.TransferDatabase get _transferDatabase => _dependencyManager.getOrCreate(); @@ -130,11 +125,12 @@ class StorageS3Service { const S3ListPluginOptions(); final resolvedPath = await _pathResolver.resolvePath(path: path); + final s3ClientInfo = getS3ClientInfo(storageBucket: options.bucket); if (!s3PluginOptions.listAll) { final request = s3.ListObjectsV2Request.build((builder) { builder - ..bucket = _storageOutputs.bucketName + ..bucket = s3ClientInfo.bucketName ..prefix = resolvedPath ..maxKeys = options.pageSize ..continuationToken = options.nextToken @@ -145,7 +141,7 @@ class StorageS3Service { try { return S3ListResult.fromPaginatedResult( - await _defaultS3Client.listObjectsV2(request).result, + await s3ClientInfo.client.listObjectsV2(request).result, ); } on smithy.UnknownSmithyHttpException catch (error) { // S3Client.headObject may return 403 error @@ -161,14 +157,14 @@ class StorageS3Service { try { final request = s3.ListObjectsV2Request.build((builder) { builder - ..bucket = _storageOutputs.bucketName + ..bucket = s3ClientInfo.bucketName ..prefix = resolvedPath ..delimiter = s3PluginOptions.excludeSubPaths ? s3PluginOptions.delimiter : null; }); - listResult = await _defaultS3Client.listObjectsV2(request).result; + listResult = await s3ClientInfo.client.listObjectsV2(request).result; recursiveResult = S3ListResult.fromPaginatedResult( listResult, ); @@ -202,12 +198,12 @@ class StorageS3Service { required StorageGetPropertiesOptions options, }) async { final resolvedPath = await _pathResolver.resolvePath(path: path); - + final s3ClientInfo = getS3ClientInfo(storageBucket: options.bucket); return S3GetPropertiesResult( storageItem: S3Item.fromHeadObjectOutput( await headObject( - s3client: _defaultS3Client, - bucket: _storageOutputs.bucketName, + s3client: s3ClientInfo.client, + bucket: s3ClientInfo.bucketName, key: resolvedPath, ), path: resolvedPath, @@ -226,9 +222,10 @@ class StorageS3Service { }) async { final s3PluginOptions = options.pluginOptions as S3GetUrlPluginOptions? ?? const S3GetUrlPluginOptions(); + final s3ClientInfo = getS3ClientInfo(storageBucket: options.bucket); if (s3PluginOptions.useAccelerateEndpoint && - _defaultS3ClientConfig.usePathStyle) { + s3ClientInfo.config.usePathStyle) { throw s3_exception.accelerateEndpointUnusable; } @@ -238,20 +235,20 @@ class StorageS3Service { // the `getProperties` API (i.e. HeadObject) await getProperties( path: path, - options: const StorageGetPropertiesOptions(), + options: StorageGetPropertiesOptions(bucket: options.bucket), ); } var resolvedPath = await _pathResolver.resolvePath(path: path); var host = - '${_storageOutputs.bucketName}.${_getS3EndpointHost(region: _storageOutputs.awsRegion)}'; - if (_defaultS3ClientConfig.usePathStyle) { - host = host.replaceFirst('${_storageOutputs.bucketName}.', ''); - resolvedPath = '${_storageOutputs.bucketName}/$resolvedPath'; + '${s3ClientInfo.bucketName}.${_getS3EndpointHost(region: s3ClientInfo.awsRegion)}'; + if (s3ClientInfo.config.usePathStyle) { + host = host.replaceFirst('${s3ClientInfo.bucketName}.', ''); + resolvedPath = '${s3ClientInfo.bucketName}/$resolvedPath'; } else if (s3PluginOptions.useAccelerateEndpoint) { // https: //docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration-getting-started.html host = host - .replaceFirst(RegExp('${_storageOutputs.awsRegion}\\.'), '') + .replaceFirst(RegExp('${s3ClientInfo.awsRegion}\\.'), '') .replaceFirst(RegExp(r'\.s3\.'), '.s3-accelerate.'); } @@ -261,10 +258,20 @@ class StorageS3Service { path: '/$resolvedPath', ); + // dependencyManager.get() is used for unit tests + final awsSigV4Signer = _dependencyManager.get() ?? + sigv4.AWSSigV4Signer( + credentialsProvider: _credentialsProvider, + ); + final signerScope = sigv4.AWSCredentialScope( + region: s3ClientInfo.awsRegion, + service: AWSService.s3, + ); + return S3GetUrlResult( - url: await _awsSigV4Signer.presign( + url: await awsSigV4Signer.presign( urlRequest, - credentialScope: _signerScope, + credentialScope: signerScope, expiresIn: s3PluginOptions.expiresIn, serviceConfiguration: _defaultS3SignerConfiguration, ), @@ -294,10 +301,11 @@ class StorageS3Service { FutureOr Function()? onDone, FutureOr Function()? onError, }) { + final s3ClientInfo = getS3ClientInfo(storageBucket: options.bucket); final downloadDataTask = S3DownloadTask( - s3Client: _defaultS3Client, - defaultS3ClientConfig: _defaultS3ClientConfig, - bucket: _storageOutputs.bucketName, + s3Client: s3ClientInfo.client, + defaultS3ClientConfig: s3ClientInfo.config, + bucket: s3ClientInfo.bucketName, path: path, options: options, pathResolver: _pathResolver, @@ -324,11 +332,13 @@ class StorageS3Service { FutureOr Function()? onDone, FutureOr Function()? onError, }) { + final s3ClientInfo = getS3ClientInfo(storageBucket: options.bucket); final uploadDataTask = S3UploadTask.fromDataPayload( dataPayload, - s3Client: _defaultS3Client, - defaultS3ClientConfig: _defaultS3ClientConfig, - bucket: _storageOutputs.bucketName, + s3Client: s3ClientInfo.client, + s3ClientConfig: s3ClientInfo.config, + bucket: s3ClientInfo.bucketName, + awsRegion: s3ClientInfo.awsRegion, path: path, options: options, pathResolver: _pathResolver, @@ -353,6 +363,7 @@ class StorageS3Service { FutureOr Function()? onDone, FutureOr Function()? onError, }) { + final s3ClientInfo = getS3ClientInfo(storageBucket: options.bucket); final s3PluginOptions = options.pluginOptions as S3UploadFilePluginOptions? ?? const S3UploadFilePluginOptions(); @@ -365,9 +376,10 @@ class StorageS3Service { ); final uploadDataTask = S3UploadTask.fromAWSFile( localFile, - s3Client: _defaultS3Client, - defaultS3ClientConfig: _defaultS3ClientConfig, - bucket: _storageOutputs.bucketName, + s3Client: s3ClientInfo.client, + s3ClientConfig: s3ClientInfo.config, + bucket: s3ClientInfo.bucketName, + awsRegion: _storageOutputs.awsRegion, path: path, options: uploadDataOptions, pathResolver: _pathResolver, @@ -400,6 +412,10 @@ class StorageS3Service { }) async { final s3PluginOptions = options.pluginOptions as S3CopyPluginOptions? ?? const S3CopyPluginOptions(); + final s3ClientInfoSource = + getS3ClientInfo(storageBucket: options.buckets?.source); + final s3ClientInfoDestination = + getS3ClientInfo(storageBucket: options.buckets?.destination); final [sourcePath, destinationPath] = await _pathResolver.resolvePaths( paths: [source, destination], @@ -407,14 +423,14 @@ class StorageS3Service { final copyRequest = s3.CopyObjectRequest.build((builder) { builder - ..bucket = _storageOutputs.bucketName - ..copySource = '${_storageOutputs.bucketName}/$sourcePath' + ..bucket = s3ClientInfoDestination.bucketName + ..copySource = '${s3ClientInfoSource.bucketName}/$sourcePath' ..key = destinationPath ..metadataDirective = s3.MetadataDirective.copy; }); try { - await _defaultS3Client.copyObject(copyRequest).result; + await s3ClientInfoDestination.client.copyObject(copyRequest).result; } on smithy.UnknownSmithyHttpException catch (error) { // S3Client.copyObject may return 403 or 404 error throw error.toStorageException(); @@ -426,8 +442,8 @@ class StorageS3Service { copiedItem: s3PluginOptions.getProperties ? S3Item.fromHeadObjectOutput( await headObject( - s3client: _defaultS3Client, - bucket: _storageOutputs.bucketName, + s3client: s3ClientInfoDestination.client, + bucket: s3ClientInfoDestination.bucketName, key: destinationPath, ), path: destinationPath, @@ -447,11 +463,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, ); @@ -479,6 +496,8 @@ class StorageS3Service { final objectIdentifiersToRemove = resolvedPaths.map((path) => s3.ObjectIdentifier(key: path)).toList(); + final s3ClientInfo = getS3ClientInfo(storageBucket: options.bucket); + final removedItems = []; final removedErrors = []; @@ -491,7 +510,7 @@ class StorageS3Service { ); final request = s3.DeleteObjectsRequest.build((builder) { builder - ..bucket = _storageOutputs.bucketName + ..bucket = s3ClientInfo.bucketName // force to use sha256 instead of md5 ..checksumAlgorithm = s3.ChecksumAlgorithm.sha256 ..delete = s3.Delete.build((builder) { @@ -499,7 +518,7 @@ class StorageS3Service { }).toBuilder(); }); try { - final output = await _defaultS3Client.deleteObjects(request).result; + final output = await s3ClientInfo.client.deleteObjects(request).result; removedItems.addAll( output.deleted?.toList().map( (removedObject) => S3Item.fromS3Object( @@ -594,21 +613,77 @@ class StorageS3Service { Future abortIncompleteMultipartUploads() async { final records = await _transferDatabase .getMultipartUploadRecordsCreatedBefore(_serviceStartingTime); - for (final record in records) { + final bucketInfo = BucketInfo( + bucketName: record.bucketName ?? _storageOutputs.bucketName, + region: record.awsRegion ?? _storageOutputs.awsRegion, + ); final request = s3.AbortMultipartUploadRequest.build((builder) { builder - ..bucket = _storageOutputs.bucketName + ..bucket = bucketInfo.bucketName ..key = record.objectKey ..uploadId = record.uploadId; }); + final s3Client = getS3ClientInfo( + storageBucket: StorageBucket.fromBucketInfo(bucketInfo), + ).client; try { - await _defaultS3Client.abortMultipartUpload(request).result; + await s3Client.abortMultipartUpload(request).result; await _transferDatabase.deleteTransferRecords(record.uploadId); } on Exception catch (error) { _logger.error('Failed to abort multipart upload due to: $error'); } } } + + /// Creates and caches [S3ClientInfo] given the optional [storageBucket] + /// parameter. If the optional parameter is not provided it uses + /// StorageOutputs default bucket to create the [S3ClientInfo]. + @internal + @visibleForTesting + S3ClientInfo getS3ClientInfo({StorageBucket? storageBucket}) { + if (storageBucket == null) { + return S3ClientInfo( + client: _defaultS3Client, + config: _defaultS3ClientConfig, + bucketName: _storageOutputs.bucketName, + awsRegion: _storageOutputs.awsRegion, + ); + } + // 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, + bucketName: bucketInfo.bucketName, + awsRegion: bucketInfo.region, + ); + _s3ClientsInfo[bucketInfo.bucketName] = s3ClientInfo; + return s3ClientInfo; + } } diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/task/s3_upload_task.dart b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/task/s3_upload_task.dart index dbecca6d44..364c74237e 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/task/s3_upload_task.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/service/task/s3_upload_task.dart @@ -48,9 +48,10 @@ const fallbackContentType = 'application/octet-stream'; class S3UploadTask { S3UploadTask._({ required s3.S3Client s3Client, - required smithy_aws.S3ClientConfig defaultS3ClientConfig, + required smithy_aws.S3ClientConfig s3ClientConfig, required S3PathResolver pathResolver, required String bucket, + required String awsRegion, required StoragePath path, required StorageUploadDataOptions options, S3DataPayload? dataPayload, @@ -59,9 +60,10 @@ class S3UploadTask { required AWSLogger logger, required transfer.TransferDatabase transferDatabase, }) : _s3Client = s3Client, - _defaultS3ClientConfig = defaultS3ClientConfig, + _s3ClientConfig = s3ClientConfig, _pathResolver = pathResolver, _bucket = bucket, + _awsRegion = awsRegion, _path = path, _options = options, _dataPayload = dataPayload, @@ -81,9 +83,10 @@ class S3UploadTask { S3UploadTask.fromDataPayload( S3DataPayload dataPayload, { required s3.S3Client s3Client, - required smithy_aws.S3ClientConfig defaultS3ClientConfig, + required smithy_aws.S3ClientConfig s3ClientConfig, required S3PathResolver pathResolver, required String bucket, + required String awsRegion, required StoragePath path, required StorageUploadDataOptions options, void Function(S3TransferProgress)? onProgress, @@ -91,9 +94,10 @@ class S3UploadTask { required transfer.TransferDatabase transferDatabase, }) : this._( s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: s3ClientConfig, pathResolver: pathResolver, bucket: bucket, + awsRegion: awsRegion, path: path, dataPayload: dataPayload, options: options, @@ -108,9 +112,10 @@ class S3UploadTask { S3UploadTask.fromAWSFile( AWSFile localFile, { required s3.S3Client s3Client, - required smithy_aws.S3ClientConfig defaultS3ClientConfig, + required smithy_aws.S3ClientConfig s3ClientConfig, required S3PathResolver pathResolver, required String bucket, + required String awsRegion, required StoragePath path, required StorageUploadDataOptions options, void Function(S3TransferProgress)? onProgress, @@ -118,9 +123,10 @@ class S3UploadTask { required transfer.TransferDatabase transferDatabase, }) : this._( s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: s3ClientConfig, pathResolver: pathResolver, bucket: bucket, + awsRegion: awsRegion, path: path, localFile: localFile, options: options, @@ -135,9 +141,10 @@ class S3UploadTask { final Completer _uploadCompleter = Completer(); final s3.S3Client _s3Client; - final smithy_aws.S3ClientConfig _defaultS3ClientConfig; + final smithy_aws.S3ClientConfig _s3ClientConfig; final S3PathResolver _pathResolver; final String _bucket; + final String _awsRegion; final StoragePath _path; final StorageUploadDataOptions _options; final void Function(S3TransferProgress)? _onProgress; @@ -191,7 +198,7 @@ class S3UploadTask { /// Should be used only internally. Future start() async { if (_s3PluginOptions.useAccelerateEndpoint && - _defaultS3ClientConfig.usePathStyle) { + _s3ClientConfig.usePathStyle) { _completeUploadWithError(s3_exception.accelerateEndpointUnusable); return; } @@ -328,7 +335,7 @@ class S3UploadTask { try { _putObjectOperation = _s3Client.putObject( putObjectRequest, - s3ClientConfig: _defaultS3ClientConfig.copyWith( + s3ClientConfig: _s3ClientConfig.copyWith( useAcceleration: _s3PluginOptions.useAccelerateEndpoint, ), ); @@ -497,6 +504,8 @@ class S3UploadTask { TransferRecord( uploadId: uploadId, objectKey: _resolvedPath, + bucketName: _bucket, + awsRegion: _awsRegion, createdAt: DateTime.now(), ), ); @@ -655,7 +664,7 @@ class S3UploadTask { try { final operation = _s3Client.uploadPart( request, - s3ClientConfig: _defaultS3ClientConfig.copyWith( + s3ClientConfig: _s3ClientConfig.copyWith( useAcceleration: _s3PluginOptions.useAccelerateEndpoint, ), ); diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/database_io.dart b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/database_io.dart index a16a66bcf3..1c105d8b32 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/database_io.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/database_io.dart @@ -35,7 +35,25 @@ class TransferDatabase extends $TransferDatabase // Bump the version number when any alteration is made into tables.dart @override - int get schemaVersion => 1; + int get schemaVersion => 2; + + @override + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (Migrator m) async { + await m.createAll(); + }, + onUpgrade: (Migrator m, int from, int to) async { + // Note: From schemaVersion 1->2 we added bucketName and awsRegion. + // they are nullable columns so that on upgrade we need to update + // the transferRecords table to add these two columns + if (from < 2) { + await m.addColumn(transferRecords, transferRecords.bucketName); + await m.addColumn(transferRecords, transferRecords.awsRegion); + } + }, + ); + } @override Future> getMultipartUploadRecordsCreatedBefore( @@ -52,6 +70,8 @@ class TransferDatabase extends $TransferDatabase objectKey: e.objectKey, uploadId: e.uploadId, createdAt: DateTime.parse(e.createdAt), + bucketName: e.bucketName, + awsRegion: e.awsRegion, ), ) .get(); @@ -63,6 +83,8 @@ class TransferDatabase extends $TransferDatabase uploadId: record.uploadId, objectKey: record.objectKey, createdAt: record.createdAt.toIso8601String(), + bucketName: Value(record.bucketName), + awsRegion: Value(record.awsRegion), ); final value = await into(transferRecords).insert(entry); return value.toString(); diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/tables.dart b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/tables.dart index 00d5b5b22d..4d13dcea2a 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/tables.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/tables.dart @@ -20,4 +20,10 @@ class TransferRecords extends Table { /// Timestamp of [uploadId] creation. TextColumn get createdAt => text()(); + + /// Amazon S3 bucket name. + TextColumn get bucketName => text().nullable()(); + + /// AWS region of Amazon S3 bucket. + TextColumn get awsRegion => text().nullable()(); } diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/tables.drift.dart b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/tables.drift.dart index 7d9037e154..babd86c0c1 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/tables.drift.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/tables.drift.dart @@ -1,10 +1,10 @@ // dart format width=80 // ignore_for_file: type=lint -import 'package:drift/drift.dart' as i0; -import 'package:amplify_storage_s3_dart/src/storage_s3_service/transfer/database/tables.drift.dart' - as i1; import 'package:amplify_storage_s3_dart/src/storage_s3_service/transfer/database/tables.dart' as i2; +import 'package:amplify_storage_s3_dart/src/storage_s3_service/transfer/database/tables.drift.dart' + as i1; +import 'package:drift/drift.dart' as i0; typedef $$TransferRecordsTableCreateCompanionBuilder = i1.TransferRecordsCompanion Function({ @@ -197,8 +197,21 @@ class $TransferRecordsTable extends i2.TransferRecords late final i0.GeneratedColumn createdAt = i0.GeneratedColumn( 'created_at', aliasedName, false, type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _bucketNameMeta = + const i0.VerificationMeta('bucketName'); + @override + late final i0.GeneratedColumn bucketName = i0.GeneratedColumn( + 'bucket_name', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _awsRegionMeta = + const i0.VerificationMeta('awsRegion'); + @override + late final i0.GeneratedColumn awsRegion = i0.GeneratedColumn( + 'aws_region', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); @override - List get $columns => [id, uploadId, objectKey, createdAt]; + List get $columns => + [id, uploadId, objectKey, createdAt, bucketName, awsRegion]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -231,6 +244,16 @@ class $TransferRecordsTable extends i2.TransferRecords } else if (isInserting) { context.missing(_createdAtMeta); } + if (data.containsKey('bucket_name')) { + context.handle( + _bucketNameMeta, + bucketName.isAcceptableOrUnknown( + data['bucket_name']!, _bucketNameMeta)); + } + if (data.containsKey('aws_region')) { + context.handle(_awsRegionMeta, + awsRegion.isAcceptableOrUnknown(data['aws_region']!, _awsRegionMeta)); + } return context; } @@ -248,6 +271,10 @@ class $TransferRecordsTable extends i2.TransferRecords .read(i0.DriftSqlType.string, data['${effectivePrefix}object_key'])!, createdAt: attachedDatabase.typeMapping .read(i0.DriftSqlType.string, data['${effectivePrefix}created_at'])!, + bucketName: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}bucket_name']), + awsRegion: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}aws_region']), ); } @@ -270,11 +297,19 @@ class TransferRecord extends i0.DataClass /// Timestamp of [uploadId] creation. final String createdAt; + + /// Amazon S3 bucket name. + final String? bucketName; + + /// AWS region of Amazon S3 bucket. + final String? awsRegion; const TransferRecord( {required this.id, required this.uploadId, required this.objectKey, - required this.createdAt}); + required this.createdAt, + this.bucketName, + this.awsRegion}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -282,6 +317,12 @@ class TransferRecord extends i0.DataClass map['upload_id'] = i0.Variable(uploadId); map['object_key'] = i0.Variable(objectKey); map['created_at'] = i0.Variable(createdAt); + if (!nullToAbsent || bucketName != null) { + map['bucket_name'] = i0.Variable(bucketName); + } + if (!nullToAbsent || awsRegion != null) { + map['aws_region'] = i0.Variable(awsRegion); + } return map; } @@ -291,6 +332,12 @@ class TransferRecord extends i0.DataClass uploadId: i0.Value(uploadId), objectKey: i0.Value(objectKey), createdAt: i0.Value(createdAt), + bucketName: bucketName == null && nullToAbsent + ? const i0.Value.absent() + : i0.Value(bucketName), + awsRegion: awsRegion == null && nullToAbsent + ? const i0.Value.absent() + : i0.Value(awsRegion), ); } @@ -302,6 +349,8 @@ class TransferRecord extends i0.DataClass uploadId: serializer.fromJson(json['uploadId']), objectKey: serializer.fromJson(json['objectKey']), createdAt: serializer.fromJson(json['createdAt']), + bucketName: serializer.fromJson(json['bucketName']), + awsRegion: serializer.fromJson(json['awsRegion']), ); } @override @@ -312,11 +361,18 @@ class TransferRecord extends i0.DataClass 'uploadId': serializer.toJson(uploadId), 'objectKey': serializer.toJson(objectKey), 'createdAt': serializer.toJson(createdAt), + 'bucketName': serializer.toJson(bucketName), + 'awsRegion': serializer.toJson(awsRegion), }; } i1.TransferRecord copyWith( - {int? id, String? uploadId, String? objectKey, String? createdAt}) => + {int? id, + String? uploadId, + String? objectKey, + String? createdAt, + i0.Value bucketName = const i0.Value.absent(), + i0.Value awsRegion = const i0.Value.absent()}) => i1.TransferRecord( id: id ?? this.id, uploadId: uploadId ?? this.uploadId, @@ -338,13 +394,16 @@ class TransferRecord extends i0.DataClass ..write('id: $id, ') ..write('uploadId: $uploadId, ') ..write('objectKey: $objectKey, ') - ..write('createdAt: $createdAt') + ..write('createdAt: $createdAt, ') + ..write('bucketName: $bucketName, ') + ..write('awsRegion: $awsRegion') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, uploadId, objectKey, createdAt); + int get hashCode => + Object.hash(id, uploadId, objectKey, createdAt, bucketName, awsRegion); @override bool operator ==(Object other) => identical(this, other) || @@ -352,7 +411,9 @@ class TransferRecord extends i0.DataClass other.id == this.id && other.uploadId == this.uploadId && other.objectKey == this.objectKey && - other.createdAt == this.createdAt); + other.createdAt == this.createdAt && + other.bucketName == this.bucketName && + other.awsRegion == this.awsRegion); } class TransferRecordsCompanion extends i0.UpdateCompanion { @@ -360,17 +421,23 @@ class TransferRecordsCompanion extends i0.UpdateCompanion { final i0.Value uploadId; final i0.Value objectKey; final i0.Value createdAt; + final i0.Value bucketName; + final i0.Value awsRegion; const TransferRecordsCompanion({ this.id = const i0.Value.absent(), this.uploadId = const i0.Value.absent(), this.objectKey = const i0.Value.absent(), this.createdAt = const i0.Value.absent(), + this.bucketName = const i0.Value.absent(), + this.awsRegion = const i0.Value.absent(), }); TransferRecordsCompanion.insert({ this.id = const i0.Value.absent(), required String uploadId, required String objectKey, required String createdAt, + this.bucketName = const i0.Value.absent(), + this.awsRegion = const i0.Value.absent(), }) : uploadId = i0.Value(uploadId), objectKey = i0.Value(objectKey), createdAt = i0.Value(createdAt); @@ -379,12 +446,16 @@ class TransferRecordsCompanion extends i0.UpdateCompanion { i0.Expression? uploadId, i0.Expression? objectKey, i0.Expression? createdAt, + i0.Expression? bucketName, + i0.Expression? awsRegion, }) { return i0.RawValuesInsertable({ if (id != null) 'id': id, if (uploadId != null) 'upload_id': uploadId, if (objectKey != null) 'object_key': objectKey, if (createdAt != null) 'created_at': createdAt, + if (bucketName != null) 'bucket_name': bucketName, + if (awsRegion != null) 'aws_region': awsRegion, }); } @@ -392,12 +463,16 @@ class TransferRecordsCompanion extends i0.UpdateCompanion { {i0.Value? id, i0.Value? uploadId, i0.Value? objectKey, - i0.Value? createdAt}) { + i0.Value? createdAt, + i0.Value? bucketName, + i0.Value? awsRegion}) { return i1.TransferRecordsCompanion( id: id ?? this.id, uploadId: uploadId ?? this.uploadId, objectKey: objectKey ?? this.objectKey, createdAt: createdAt ?? this.createdAt, + bucketName: bucketName ?? this.bucketName, + awsRegion: awsRegion ?? this.awsRegion, ); } @@ -416,6 +491,12 @@ class TransferRecordsCompanion extends i0.UpdateCompanion { if (createdAt.present) { map['created_at'] = i0.Variable(createdAt.value); } + if (bucketName.present) { + map['bucket_name'] = i0.Variable(bucketName.value); + } + if (awsRegion.present) { + map['aws_region'] = i0.Variable(awsRegion.value); + } return map; } @@ -425,7 +506,9 @@ class TransferRecordsCompanion extends i0.UpdateCompanion { ..write('id: $id, ') ..write('uploadId: $uploadId, ') ..write('objectKey: $objectKey, ') - ..write('createdAt: $createdAt') + ..write('createdAt: $createdAt, ') + ..write('bucketName: $bucketName, ') + ..write('awsRegion: $awsRegion') ..write(')')) .toString(); } diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/transfer_record.dart b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/transfer_record.dart index 3607e8a0ed..e5911c8d22 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/transfer_record.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/transfer_record.dart @@ -18,6 +18,8 @@ class TransferRecord { required this.uploadId, required this.objectKey, required this.createdAt, + this.bucketName, + this.awsRegion, }); /// creates new [TransferRecord] object from a [json] map. @@ -40,6 +42,12 @@ class TransferRecord { /// Timestamp of [uploadId] creation. final DateTime createdAt; + /// Amazon S3 bucket name. + final String? bucketName; + + /// AWS region of Amazon S3 bucket. + final String? awsRegion; + /// return json map representation of [TransferRecord] object. Map toJson() => _$TransferRecordToJson(this); diff --git a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/transfer_record.g.dart b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/transfer_record.g.dart index 270e70dd8f..d9783e3a7c 100644 --- a/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/transfer_record.g.dart +++ b/packages/storage/amplify_storage_s3_dart/lib/src/storage_s3_service/transfer/database/transfer_record.g.dart @@ -11,6 +11,8 @@ TransferRecord _$TransferRecordFromJson(Map json) => uploadId: json['uploadId'] as String, objectKey: json['objectKey'] as String, createdAt: DateTime.parse(json['createdAt'] as String), + bucketName: json['bucketName'] as String?, + awsRegion: json['awsRegion'] as String?, ); Map _$TransferRecordToJson(TransferRecord instance) => @@ -18,4 +20,6 @@ Map _$TransferRecordToJson(TransferRecord instance) => 'uploadId': instance.uploadId, 'objectKey': instance.objectKey, 'createdAt': instance.createdAt.toIso8601String(), + 'bucketName': instance.bucketName, + 'awsRegion': instance.awsRegion, }; diff --git a/packages/storage/amplify_storage_s3_dart/pubspec.yaml b/packages/storage/amplify_storage_s3_dart/pubspec.yaml index ba60653254..f831cba76d 100644 --- a/packages/storage/amplify_storage_s3_dart/pubspec.yaml +++ b/packages/storage/amplify_storage_s3_dart/pubspec.yaml @@ -1,6 +1,6 @@ name: amplify_storage_s3_dart description: A Dart-only implementation of the Amplify Storage plugin for S3. -version: 0.4.6 +version: 0.4.8 homepage: https://docs.amplify.aws/lib/q/platform/flutter/ repository: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/storage/amplify_storage_s3_dart issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues @@ -9,8 +9,8 @@ environment: sdk: ^3.5.0 dependencies: - amplify_core: ">=2.5.0 <2.6.0" - amplify_db_common_dart: ">=0.4.7 <0.5.0" + amplify_core: ">=2.6.0 <2.7.0" + amplify_db_common_dart: ">=0.4.8 <0.5.0" async: ^2.10.0 aws_common: ">=0.7.3 <0.8.0" aws_signature_v4: ">=0.6.3 <0.7.0" 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 17015eb5f7..db95a371d2 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 @@ -140,6 +140,9 @@ void main() { const testOptions = StorageListOptions( pluginOptions: S3ListPluginOptions(excludeSubPaths: true), nextToken: 'next-token-123', + bucket: StorageBucket.fromBucketInfo( + BucketInfo(bucketName: 'unit-test-bucket', region: 'us-east-2'), + ), pageSize: 2, ); @@ -245,6 +248,9 @@ void main() { () async { const testOptions = StorageGetPropertiesOptions( pluginOptions: S3GetPropertiesPluginOptions(), + bucket: StorageBucket.fromBucketInfo( + BucketInfo(bucketName: 'unit-test-bucket', region: 'us-east-2'), + ), ); when( @@ -342,6 +348,9 @@ void main() { expiresIn: Duration(minutes: 10), useAccelerateEndpoint: true, ), + bucket: StorageBucket.fromBucketInfo( + BucketInfo(bucketName: 'unit-test-bucket', region: 'us-east-2'), + ), ); when( @@ -461,6 +470,9 @@ void main() { useAccelerateEndpoint: true, getProperties: true, ), + bucket: StorageBucket.fromBucketInfo( + BucketInfo(bucketName: 'unit-test-bucket', region: 'us-east-2'), + ), ); when( @@ -534,6 +546,14 @@ void main() { const StorageUploadDataOptions(), ); registerFallbackValue(const S3DataPayload.empty()); + registerFallbackValue( + const StorageBucket.fromBucketInfo( + BucketInfo( + bucketName: 'bucketName', + region: 'region', + ), + ), + ); }); test('should forward default options to StorageS3Service.uploadData API', @@ -595,6 +615,12 @@ void main() { test('should forward options to StorageS3Service.uploadData API', () async { const testOptions = StorageUploadDataOptions( + bucket: StorageBucket.fromBucketInfo( + BucketInfo( + bucketName: 'test-bucket', + region: 'test-region', + ), + ), pluginOptions: S3UploadDataPluginOptions( getProperties: true, useAccelerateEndpoint: true, @@ -770,6 +796,9 @@ void main() { getProperties: true, useAccelerateEndpoint: true, ), + bucket: StorageBucket.fromBucketInfo( + BucketInfo(bucketName: 'unit-test-bucket', region: 'us-east-2'), + ), ); when( @@ -964,7 +993,10 @@ void main() { ), ).thenAnswer((_) async => testResult); - final removeOperation = storageS3Plugin.remove(path: testPath); + final removeOperation = storageS3Plugin.remove( + path: testPath, + options: defaultOptions, + ); final capturedOptions = verify( () => storageS3Service.remove( @@ -990,6 +1022,9 @@ void main() { test('should forward options to StorageS3Service.remove() API', () async { const testOptions = StorageRemoveOptions( pluginOptions: S3RemovePluginOptions(), + bucket: StorageBucket.fromBucketInfo( + BucketInfo(bucketName: 'unit-test-bucket', region: 'us-east-2'), + ), ); when( @@ -1084,6 +1119,9 @@ void main() { () async { const testOptions = StorageRemoveManyOptions( pluginOptions: S3RemoveManyPluginOptions(), + bucket: StorageBucket.fromBucketInfo( + BucketInfo(bucketName: 'unit-test-bucket', region: 'us-east-2'), + ), ); when( 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 aec65db336..0ca3760d5e 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 @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:amplify_core/amplify_core.dart' hide PaginatedResult; +import 'package:amplify_core/src/config/amplify_outputs/storage/bucket_outputs.dart'; import 'package:amplify_core/src/config/amplify_outputs/storage/storage_outputs.dart'; import 'package:amplify_storage_s3_dart/amplify_storage_s3_dart.dart'; import 'package:amplify_storage_s3_dart/src/exception/s3_storage_exception.dart'; @@ -25,9 +26,18 @@ const testPath = StoragePath.fromString('some/path.txt'); void main() { group('StorageS3Service', () { const testBucket = 'bucket1'; + const testBucketName = 'bucket1-name'; const testRegion = 'west-2'; - const storageOutputs = - StorageOutputs(bucketName: testBucket, awsRegion: testRegion); + const testBuckets = BucketOutputs( + name: testBucket, + bucketName: testBucketName, + awsRegion: testRegion, + ); + const storageOutputs = StorageOutputs( + bucketName: testBucket, + awsRegion: testRegion, + buckets: [testBuckets], + ); final pathResolver = TestPathResolver(); late DependencyManager dependencyManager; @@ -35,14 +45,20 @@ void main() { late StorageS3Service storageS3Service; late AWSLogger logger; late AWSSigV4Signer awsSigV4Signer; + late AmplifyUserAgent mockUserAgent; + late AWSHttpClient mockAwsHttpClient; setUp(() { s3Client = MockS3Client(); logger = MockAWSLogger(); awsSigV4Signer = MockAWSSigV4Signer(); + mockUserAgent = MockAmplifyUserAgent(); + mockAwsHttpClient = MockAWSHttpClient(); dependencyManager = DependencyManager() ..addInstance(s3Client) - ..addInstance(awsSigV4Signer); + ..addInstance(awsSigV4Signer) + ..addInstance(mockUserAgent) + ..addInstance(mockAwsHttpClient); storageS3Service = StorageS3Service( storageOutputs: storageOutputs, pathResolver: pathResolver, @@ -69,6 +85,19 @@ void main() { expect(message, contains('Since your bucket name contains dots')); }); + test('creates and caches s3 client info for each storage bucket', () { + final client1 = storageS3Service.getS3ClientInfo( + storageBucket: const StorageBucket.fromBucketInfo( + BucketInfo(bucketName: testBucketName, region: testRegion), + ), + ); + final client2 = storageS3Service.getS3ClientInfo( + storageBucket: StorageBucket.fromOutputs(testBucket), + ); + + expect(client1, client2); + }); + group('list() API', () { late S3ListResult listResult; const testNextContinuationToken = 'get-next-page'; diff --git a/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/task/s3_upload_task_test.dart b/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/task/s3_upload_task_test.dart index 51d373d0c5..85e96ec5d9 100644 --- a/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/task/s3_upload_task_test.dart +++ b/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/task/s3_upload_task_test.dart @@ -28,6 +28,7 @@ void main() { late AWSLogger logger; late transfer.TransferDatabase transferDatabase; const testBucket = 'fake-bucket'; + const testRegion = 'test-region'; const defaultS3ClientConfig = smithy_aws.S3ClientConfig(); final pathResolver = TestPathResolver(); const testUploadDataOptions = StorageUploadDataOptions(); @@ -113,9 +114,10 @@ void main() { final uploadDataTask = S3UploadTask.fromDataPayload( testDataPayload, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: testPath, options: const StorageUploadDataOptions(), logger: logger, @@ -172,9 +174,10 @@ void main() { final uploadDataTask = S3UploadTask.fromDataPayload( testDataPayload, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: testPath, options: testUploadDataOptions, logger: logger, @@ -222,9 +225,10 @@ void main() { final uploadDataTask = S3UploadTask.fromDataPayload( testDataPayloadBytes, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: testPath, options: testUploadDataOptions, logger: logger, @@ -291,9 +295,10 @@ void main() { final uploadDataTask = S3UploadTask.fromDataPayload( testDataPayload, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: testPath, options: testUploadDataOptions, logger: logger, @@ -334,9 +339,10 @@ void main() { final uploadDataTask = S3UploadTask.fromDataPayload( testDataPayload, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: testPath, options: testUploadDataOptions, logger: logger, @@ -369,9 +375,10 @@ void main() { final uploadDataTask = S3UploadTask.fromDataPayload( testDataPayload, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: testPath, options: testUploadDataOptions, logger: logger, @@ -414,9 +421,10 @@ void main() { final uploadDataTask = S3UploadTask.fromDataPayload( testDataPayload, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: testPath, options: testUploadDataOptions, logger: logger, @@ -466,9 +474,10 @@ void main() { final uploadDataTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -526,9 +535,10 @@ void main() { final uploadDataTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -582,9 +592,10 @@ void main() { final uploadDataTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -636,9 +647,10 @@ void main() { final uploadDataTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -774,9 +786,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -956,9 +969,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1048,9 +1062,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFileWithoutContentType, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1149,9 +1164,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1186,9 +1202,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1218,9 +1235,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testBadFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1319,9 +1337,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( mockFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1349,9 +1368,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1393,9 +1413,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: const StorageUploadDataOptions(), logger: logger, @@ -1436,9 +1457,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1479,9 +1501,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1560,9 +1583,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1652,9 +1676,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: const StorageUploadDataOptions(), logger: logger, @@ -1743,9 +1768,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1813,9 +1839,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -1969,9 +1996,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -2027,9 +2055,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( testLocalFile, s3Client: s3Client, - defaultS3ClientConfig: defaultS3ClientConfig, + s3ClientConfig: defaultS3ClientConfig, pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: testUploadDataOptions, logger: logger, @@ -2091,10 +2120,10 @@ void main() { final uploadTask = S3UploadTask.fromAWSFile( AWSFile.fromPath('fake/file.jpg'), s3Client: s3Client, - defaultS3ClientConfig: - const smithy_aws.S3ClientConfig(usePathStyle: true), + s3ClientConfig: const smithy_aws.S3ClientConfig(usePathStyle: true), pathResolver: pathResolver, bucket: testBucket, + awsRegion: testRegion, path: const StoragePath.fromString(testKey), options: const StorageUploadDataOptions( pluginOptions: S3UploadDataPluginOptions( diff --git a/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/transfer/database_html_test.dart b/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/transfer/database_html_test.dart index 73ae16eb75..9a92df808e 100644 --- a/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/transfer/database_html_test.dart +++ b/packages/storage/amplify_storage_s3_dart/test/storage_s3_service/transfer/database_html_test.dart @@ -12,12 +12,16 @@ void main() { group('TransferDatabase for web', () { const testUploadId = 'test-upload-Id'; const testObjectKey = 'test-object-Key'; + const testBucketName = 'test-bucket-name'; + const testAwsRegion = 'test-aws-region'; final testCreatedAt = DateTime(2022, 1, 1); final testTransferRecord = TransferRecord( uploadId: testUploadId, objectKey: testObjectKey, createdAt: testCreatedAt, + bucketName: testBucketName, + awsRegion: testAwsRegion, ); final testTransferRecordJsonString = testTransferRecord.toJsonString(); diff --git a/packages/storage/amplify_storage_s3_dart/test/test_utils/mocks.dart b/packages/storage/amplify_storage_s3_dart/test/test_utils/mocks.dart index 1136704cde..1954fb75fa 100644 --- a/packages/storage/amplify_storage_s3_dart/test/test_utils/mocks.dart +++ b/packages/storage/amplify_storage_s3_dart/test/test_utils/mocks.dart @@ -27,3 +27,7 @@ class MockS3UploadTask extends Mock implements S3UploadTask {} class MockTransferDatabase extends Mock implements TransferDatabase {} class MockSmithyOperation extends Mock implements SmithyOperation {} + +class MockAmplifyUserAgent extends Mock implements AmplifyUserAgent {} + +class MockAWSHttpClient extends Mock implements AWSHttpClient {} diff --git a/tool/test_all_plugins.sh b/tool/test_all_plugins.sh index 6ff3297668..9ba8880fa1 100755 --- a/tool/test_all_plugins.sh +++ b/tool/test_all_plugins.sh @@ -55,7 +55,7 @@ case $KIND in fi cd ios - XCODEBUILD_DESTINATION="platform=iOS Simulator,name=iPhone 14 Pro Max,OS=latest" + XCODEBUILD_DESTINATION="platform=iOS Simulator,name=iPhone 16 Pro Max,OS=latest" if xcodebuild test \ -workspace Runner.xcworkspace \ -scheme Runner \