Skip to content

feat(amplify_storage_s3): UploadFile and GetUrl #15

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from

Conversation

Ashish-Nanda
Copy link
Contributor

Description of changes:
Draft PR that adds support for UploadFile and GetUrl functionality for Amplify Flutter.
Todo:
Error handling improvements, tests and other refinement mentioned in TODOs

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@Ashish-Nanda Ashish-Nanda requested a review from Amplifiyer August 7, 2020 22:48
Comment on lines +1 to +13
// import 'package:flutter_test/flutter_test.dart';

// import 'package:amplify_storage_plugin_interface/amplify_storage_plugin_interface.dart';

// void main() {
// test('adds one to input values', () {
// final calculator = Calculator();
// expect(calculator.addOne(2), 3);
// expect(calculator.addOne(-7), -6);
// expect(calculator.addOne(0), 1);
// expect(() => calculator.addOne(null), throwsNoSuchMethodError);
// });
// }
Copy link
Contributor

Choose a reason for hiding this comment

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

probably should delete the file if not needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, delete this whole test directory if we don't have actual tests.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

let req = FlutterGetUrlRequest(request: request)
print("GetUrlRequest.Options object is: \(String(describing: req.options))")
_ = Amplify.Storage.getURL(key: req.key,
options: nil,
Copy link
Contributor

Choose a reason for hiding this comment

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

not req.options?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Draft PR. Lot of this PR is WIP but I wanted you to get your eyes on it before you leave.

Copy link
Contributor

Choose a reason for hiding this comment

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

Definitely. I didn't see a Todo on this one that's why left a comment to keep track

var options: StorageUploadFileRequest.Options? = nil
init(request: Dictionary<String, AnyObject>) {
self.key = request["key"] as! String
self.file = NSURL(fileURLWithPath: request["path"] as! String) as URL
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be validated that the path is valid? in the isValid() before init

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I can add that to isValid()

}
}

Future<UploadFileResponse> uploadFile({@required UploadFileRequest request}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Assuming that this is where a subsequent PR will allow discrete parameters.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yea -- where did you guys land with this? In the design review, we had proposed to do a 1 pager weighing pros and cons. Were there any cons to "exploding" the common args into the method signature? Or what's the status?

Copy link
Contributor Author

@Ashish-Nanda Ashish-Nanda Aug 14, 2020

Choose a reason for hiding this comment

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

We are doing it. For now we are just creating the request instance inside the method and exposing an interface with named parameters to the customer. I dont have that in this PR, it will be a followup.

No real downside to that, and definite upside of better customer experience.

@@ -0,0 +1,6 @@
class GetUrlResponse {
Copy link
Contributor

Choose a reason for hiding this comment

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

Assuming that closing on this will be a subsequent PR prior to P0.

Copy link
Contributor

Choose a reason for hiding this comment

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

^ @haverchuck Closing on what?

Map<String, dynamic> serializeAsMap() {
final Map<String, dynamic> optionsMap = <String, dynamic>{};
if (accessLevel != null) {
optionsMap["accessLevel"] = describeEnum(accessLevel);
Copy link
Contributor

Choose a reason for hiding this comment

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

nice

@@ -0,0 +1,10 @@
export 'Storage/StorageAccessLevel.dart';
export 'Storage/StorageOptions.dart';

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: as these type files grow, it can be helpful to create little comment headings (IMO)

Copy link
Contributor

Choose a reason for hiding this comment

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

Keeping diligent about the use of whitespace also does work. The comments could end up like:

// Do the needful
export 'do'
export 'the'
export 'needful'

Or, I guess you could document the grouping strategy, once, at the top of the document. That could be frugal.


fun uploadFile(@NonNull flutterResult: MethodChannel.Result, @NonNull request: HashMap<String, *>, mainActivity: Activity?) {
if (FlutterUploadFileRequest.isValid(request)) {
Log.i("AmplifyFlutter", "uploadFile request:" + request.toString())
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be helpful to use amplify-android's logging utility.

@@ -0,0 +1,153 @@
PODS:
Copy link
Contributor

Choose a reason for hiding this comment

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

This file should not be checked in.

Pod::Spec.new do |s|
s.name = 'amplify_storage_s3'
s.version = '0.0.1'
s.summary = 'A new flutter plugin project.'
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to update this boilerplate.


environment:
sdk: ">=2.7.0 <3.0.0"
flutter: ">=1.10.0"
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to bump this to >=1.20.0 and retest

@@ -0,0 +1,23 @@
// import 'package:flutter/services.dart';
Copy link
Contributor

Choose a reason for hiding this comment

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

Assuming that we will have tests here prior to p0; ditto for android unit tests.

Copy link
Contributor

@haverchuck haverchuck left a comment

Choose a reason for hiding this comment

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

Left some minor requests for revisions.

plugin:
platforms:
android:
package: com.amazonaws.amplify.amplify_storage_s3
Copy link
Contributor

Choose a reason for hiding this comment

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

com.amplifyframework.flutter.storage.s3?

Comment on lines +3 to +6
class StorageCategory {
const StorageCategory();

static List<StoragePluginInterface> plugins = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

In iOS and Android, the category and plugin both extend a common interface. This ensures that the customer APIs are fully addressed by the plugin developer. How come you all decided not to pursue this pattern for Flutter?

}
}

Future<UploadFileResponse> uploadFile({@required UploadFileRequest request}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Yea -- where did you guys land with this? In the design review, we had proposed to do a 1 pager weighing pros and cons. Were there any cons to "exploding" the common args into the method signature? Or what's the status?


Future<GetUrlResponse> getUrl({@required GetUrlRequest request}) {
/// call `getUrl` on the plugin
return plugins[0].getUrl(request: request);
Copy link
Contributor

Choose a reason for hiding this comment

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

Android and iOS have a method for this purpose, like getSelectedPlugin(). It does range checking on the list, and if there is not any plugin, will return a friendly message: "Kind friend, please consider the opportunity to add a plugin." etc. A little better than "0 not in range."

@@ -0,0 +1,3 @@
## [0.0.1] - TODO: Add release date.
Copy link
Contributor

Choose a reason for hiding this comment

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

I would just delete this file until you have real content for it.

}
)
} catch (e: Exception) {
prepareError(flutterResult, "ERROR_ENCOUNTERED_IN_GET_URL_OPERATION", e.localizedMessage, "")
Copy link
Contributor

Choose a reason for hiding this comment

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

These screaming snake strings are weird. They look like they want to be constants in the language, but yet they are strings. Is the intention to show these to a user? If so, style these appropriately for consumption as natural langauge.

An error was enountered in the GetUrl operation.

instead of

ERROR_ENCOUNTERED_IN_GET_URL_OPERATION

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are correct. I Intend to make them constants. There is a TODO uptop, line 17.

private fun prepareGetUrlResponse(@NonNull flutterResult: MethodChannel.Result, result: StorageGetUrlResult) {
Handler (Looper.getMainLooper()).post {
Log.i("AmplifyFlutter", "Successfully got Url: " + result.url.toString())
val response: HashMap<String, String> = HashMap()
Copy link
Contributor

Choose a reason for hiding this comment

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

Liskov Principle.

Map on LHS. Implementation type on RHS. Same comment for entire code base.

}
}

//TODO: Move to Error or Util Class
Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, well, maybe. Right now, you have one method inside of a companion object, and then this method is at global scope. Shouldn't they at least be the same? Why is this not in the companion object, or why is the companion method not at global scope?

Handler (Looper.getMainLooper()).post {
Log.i("AmplifyFlutter", "Error: " + localizedError + msg);
var errorDetails: HashMap<String, Any> = HashMap();
errorDetails.put("PLATFORM_EXCEPTIONS", mapOf(
Copy link
Contributor

Choose a reason for hiding this comment

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

If someone will be expected to reliably read this key back out, don't use an inline string constant. Instead, wearhouse this value as a public constant somewhere. Perhaps in the companion object, perhaps in a separate enum somewhere, perhaps as a StringDef. But, as a consumer I'm going to do:

errorDetails.get("PLATFORM_EXCEPTIONSSS") // Ooops, this doesn't work at runtime and I get confused because my project has thousands of lines of code -- n^2 complexity to this problem, the more runtime errors I ahve instead of compile time

val key: String = request["key"] as String
val options: StorageGetUrlOptions = setOptions(request)

private fun setOptions(request: HashMap<String, *>): StorageGetUrlOptions {
Copy link
Contributor

Choose a reason for hiding this comment

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

Map, Liskov

result(FlutterMethodNotImplemented)
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change

}
return nil
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change


private func setOptions(request: Dictionary<String, AnyObject>) -> StorageUploadFileRequest.Options? {

if(request["options"] != nil) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if(request["options"] != nil) {
if (request["options"] != nil) {

}

private func setOptions(request: Dictionary<String, AnyObject>) -> StorageUploadFileRequest.Options? {

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change


static func isValid(request: Dictionary<String, AnyObject>) -> Bool {
var valid: Bool = true;
if(!(request["key"] != nil && request["key"] is String)){
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if(!(request["key"] != nil && request["key"] is String)){
if (!(request["key"] != nil && request["key"] is String)) {

apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 28
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd target 30. Make sure target and compile versions match.

val file: File = File(request["path"] as String)
val options: StorageUploadFileOptions = setOptions(request)

private fun setOptions(request: HashMap<String, *>): StorageUploadFileOptions {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use just the Map interface, for all method specs. HashMap only gets used when you're creating an instance.

Same comment for entire code base, not just this file.

val accessLevel: StorageAccessLevel? = StorageAccessLevel.values().find { it.name == accessLevelStringOption.toUpperCase() }
options.accessLevel(accessLevel)
}
"targetIdentityId" -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you get these strings into an enum? Using string literals like this is error prone, if you'll use them in more than one place.

}
}

fun getUrl(@NonNull flutterResult: MethodChannel.Result, @NonNull request: HashMap<String, *>, mainActivity: Activity?) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove Activity from the method spec.

import io.flutter.plugin.common.PluginRegistry.Registrar


class AmplifyStorageS3Plugin : FlutterPlugin, ActivityAware, MethodCallHandler {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you need to implement ActivityAware, or are you doing this because you wanted to use a mainActivity to exploit its runOnUiThread?

If at all possible, don't mess with an Activity, at all. You can run on the main thread via Handler(Looper.getMainLooper()). But even for that, make sure you have to. You have to, for UI operations. That's about it.

@Ashish-Nanda
Copy link
Contributor Author

Created a PR off a new branch where I addressed most of the above comments along with other improvements. Closing this draft PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants