Skip to content
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

AppSync, S3 and React Native (Expo) #183

Closed
mlecoq opened this issue Jul 16, 2018 · 34 comments
Closed

AppSync, S3 and React Native (Expo) #183

mlecoq opened this issue Jul 16, 2018 · 34 comments
Assignees

Comments

@mlecoq
Copy link

mlecoq commented Jul 16, 2018

Hi,

I do not succeed in uploading pictures on s3 on my react native app (see https://stackoverflow.com/questions/51366876/aws-appsync-cognito-s3)

I have tried many ways to put params for localUri ( {fileName, mimeType} like in https://github.com/aws-samples/aws-amplify-graphql, base64, file uri ...). There is no information in appsync logs to explain why file is not uploaded to s3 (whereas an entry is added in dynamodb table). Looking in redux inspector, I see commit_offline_mutation actions like if everything worked fine. File upload silently fails.

sdk should warn us when file upload is not possible.

It could help us to figure out which mistake has been made.

@mlecoq
Copy link
Author

mlecoq commented Jul 17, 2018

I have succeeded in uploading my file, but associated dynamodb is not created

@mlecoq
Copy link
Author

mlecoq commented Jul 17, 2018

In fact here is my schema

input PictureInput {
  ede_number: String!
  file: S3ObjectInput!
}

enum Visibility {
  public
  private
}

type S3Object {
  bucket: String!
  region: String!
  key: String!
}

input S3ObjectInput {
  bucket: String!
  region: String!
  localUri: String
  visibility: Visibility
  key: String
  mimeType: String
}

type Picture {
  picture_id: String!
  ede_number: String!
  file: S3Object
}

So I have a picture object with a file sub-object
in complex-object-link there is this code

  uploadPromise = Promise.resolve(uploadCredentials).then(credentials => upload(fileField, { credentials }).then(() => {
                    const { bucket, key, region } = fileField;
                    operation.variables[fileFieldKey] = { bucket, key, region };

                    return operation;
                }).catch(err => {
                    const error = new GraphQLError(err.message);
                    error.errorType = 'AWSAppSyncClient:S3UploadException'

                    throw new ApolloError({
                        graphQLErrors: [error],
                    });
                }));

which causes the following error:

"GraphQL error: The variables input contains a field name 'bucket' that is not defined for input object type 'PictureInput' "

because fileFieldKey is picture and it replaces picture with { bucket, key, region } (instead of file)

@mlecoq
Copy link
Author

mlecoq commented Jul 17, 2018

There is a solution by splitting Picture and S3Object and having 2 variables in mutation instead of one.

My current mutation is :

addPicture(picture: PictureInput!): Picture

changing it in

addPicture(file: S3ObjectInput!, ede_number: String!): Picture will solve my case. Anyway, I think the sdk should support file sub objects. If you agree, I could make a PR.

@mlecoq
Copy link
Author

mlecoq commented Jul 30, 2018

Pull request made : #204

@serendipity1004
Copy link

@mlecoq how did you exactly form the file object? I am trying to upload to S3 with react native without success for hours. I am providing bucket, region, key and localUri of my file that I retrieved from Image Picker. I get the document created in DynamoDB; however, I don't get the file uploaded to S3.

@mlecoq
Copy link
Author

mlecoq commented Jul 31, 2018

In your schema, you have to define one of your mutation variables as {key, bucket, region}.

On client side, you have to send this variable as {localUri, mimeType, key, bucket, region} : appsync sdk will use localUri and mimeType to upload the file to s3 bucket (and then will only send key, bucket and region infos to appsync endpoint)

@serendipity1004
Copy link

@mlabieniec I really feel like I'm going crazy now. It would be great if you can be kind enough to take a look at this just one more time.

I have my inputs looking like below

let post = {
    title: 'title',
    creator: 'Ji Ho',
    content: 'content'
}

let images = {
    bucket:'bucket-name'
    key:'key' - I auto id this in resovler
    region : 'ap-northeast-1'
    mimeType:'image/jpeg'
}

I have a resolver that looks like below on createPost

## Recreate input object
#set($post = {})
#set($postInput = $ctx.args.post)

$util.qr($post.put("title", $postInput.title))
$util.qr($post.put("content", $postInput.content))
$util.qr($post.put("type", $postInput.type))
$util.qr($post.put("creator", $ctx.identity.username))
$util.qr($post.put("createdAt", $util.time.nowISO8601()))
$util.qr($post.put("like", 0))
$util.qr($post.put("dislike", 0))
$util.qr($post.put("frozen", false))

## Recreate S3Object
#set($imagesArr = [])
#foreach($image in $ctx.args.images)
	$util.qr($imagesArr.add($util.dynamodb.toS3Object($util.autoId(), "bucket", "region", "0")))
#end
$util.qr($post.put("images", $imagesArr))

{
    "version" : "2017-02-28",
    "operation" : "PutItem",
    "key" : {
        "id": $util.dynamodb.toDynamoDBJson($util.autoId()),
    },
    "attributeValues" : $util.dynamodb.toMapValuesJson($post)
}

I get this list inserted to my images column

[  { "M" : {      "S" : { "S" : "{\"s3\":{\"key\":\"key\",\"bucket\":\"bucket\",\"region\":\"region\",\"version\":\"0\"}}" }    }  }]

The 'bucket', 'region' are correctly set to my bucket name and my bucket region of course.

I think the only difference with your input and mine is whether the images are in array or a single object. If you have any suggestions to what might be going wrong, please tell me. Thank you very much!

@mlecoq
Copy link
Author

mlecoq commented Jul 31, 2018

Array are not supported. In fact the sdk check if one of your mutation variables has the format {key, bucket ...} to upload files. So array are not viewed

@mlecoq
Copy link
Author

mlecoq commented Jul 31, 2018

See #161

@serendipity1004
Copy link

@mlecoq it is unbelievably still not working... document is created just fine with S3Object but no upload... I have no single idea why

@mlecoq
Copy link
Author

mlecoq commented Jul 31, 2018

Put some logs in complex-object-link.js in node-modules/AWS-appsync/lib directory, especially in catch blocs to see which errors are raised

@manueliglesias
Copy link
Contributor

Hi @serendipity1004

As @mlecoq correctly pointed out, the SDK only uploads the file to S3 if all 5 keys are present:

On client side, you have to send this variable as {localUri, mimeType, key, bucket, region} : appsync sdk will use localUri and mimeType to upload the file to s3 bucket (and then will only send key, bucket and region infos to appsync endpoint)

{localUri, mimeType, key, bucket, region}. These keys are all needed to call S3. localUri can be anything the S3 client accepts:

Body — (Buffer, Typed Array, Blob, String, ReadableStream)

I am working on adding unit tests for this logic (so you can see clearly what is expected in terms of variables), I'll reference this issue in the PR once ready.

That same PR will probably add support for arrays (#161)

@dhiraj-belkhode
Copy link

@manueliglesias please have a look into #179

@motae99
Copy link

motae99 commented Oct 29, 2018

providing all above still doesn't work, entry is created but file is not uploaded

@mikeRChambers610
Copy link

Hi @mlecoq - it seems you have got the S3Object uploading successfully through the AppSync resolver to S3 and automatically storing the url as an item in the DynamoDB table?

If so, I would really appreciate it if you could share your code with us. I am looking for an example of the react native client side uploading (my upload has an undefinded mimeType. Is this expected?).

I see the mutation you are using, could you share your resolver? Also, is your S3 bucket public? I can not figure out how to authorize the cognito user other than using the docs ( complexObjectsCredentials: () => Auth.currentCredentials(),).

Thanks in advance!!

@mlecoq
Copy link
Author

mlecoq commented Dec 11, 2018

@mikeRChambers610

S3 bucket is not public, in fact I had to use federated identities (cognito only is not sufficient) to associate a role with grant access to bucket. The resolver :

On appsync I does not receive the file but an object (with bucket, region and key):

type File {
  bucket: String!
  region: String!
  key: String!
}

input PictureInput {
  bucket: String!
  region: String!
  key: String!
}

type Picture {
  picture_id: String!
  file: File
}


type Mutation {
  addPicture(picture: PictureInput!): Picture
}

@motae99
Copy link

motae99 commented Dec 26, 2018 via email

@motae99
Copy link

motae99 commented Dec 26, 2018 via email

@mikechambers610
Copy link

mikechambers610 commented Jan 3, 2019

@mlecoq @motae99 Thank you for the responses. I appreciate all of the help that I can get. My react native app is using cognito user pools and has been working fine. For testing purposes my S3 bucket is public. Would you please review my configuration below and let me know where I might be going wrong? Thanks a lot!

REACT NATIVE METHOD (No error but nothing happens on the backend) --

sendPicS3 = async () => {

let file;
let uri = this.state.result;

 if (uri) { // selectedFile is the file to be uploaded, typically comes from an <input type="file" />
   const { name, type: mimeType } = uri;
   const [, , , extension] = /([^.]+)(\.(\w+))?$/.exec(name);

   const bucket = AWSConfig.aws_content_delivery_bucket;
   const region = AWSConfig.aws_content_delivery_bucket_region;
   const key = [uuidV4(), extension].filter(x => !!x).join('.');

   file = {
     bucket,
     key,
     region,
     mimeType,
     localUri: uri,
   };

   console.log(file)

   this.props.onCreateTestFile({
     username: this.state.username,
     file: file,
   })

}
}

REACT NATIVE GRAPHQL MUTATION --

import graphql from 'graphql-tag'

export default graphqlmutation CreateTestFile ($username: String!, $file: S3ObjectInput!){ createTestFile( input: {username: $username, file: $file} ) { username file } }

APPSYNC SCHEMA

input CreateTestFileInput {
username: String!
file: S3ObjectInput!
}

type S3Object {
bucket: String!
region: String!
key: String!
}

input S3ObjectInput {
bucket: String!
region: String!
localUri: String
visibility: Visibility
key: String
mimeType: String
}

APPSYNC MUTATION

createTestFile(username: String!, file: S3ObjectInput!): TestFile

APPSYNC RESOLVER

{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.username),
},

#set( $attribs = $util.dynamodb.toMapValues($ctx.args.input) )
#set( $file = $ctx.args.input.file )
#set( $attribs.file = $util.dynamodb.toS3Object($file.key, $file.bucket, $file.region) )

"attributeValues": $util.toJson($attribs)

}

APPSYNC RESPONSE RESOLVER

{
"version": "2017-02-28",
"payload": {}
}

$util.toJson($util.dynamodb.fromS3ObjectJson($context.source.file))

@motae99
Copy link

motae99 commented Jan 3, 2019 via email

@mikechambers610
Copy link

Thanks @motae99 I would appreciate if you could share your code that would help me a lot.
I am kind of new to the file transfer aspect and using AppSync is just throwing me through a loop.

@mikechambers610
Copy link

@motae99 I am using the ImagePicker and storing the uri for the image selected in this.state.result.
After that I am storing this.state.result as "uri" and passing that directly. I think that is probably the wrong way as you as saying it seems like I need to be converting the file just not sure how to do that. Thanks!!

@motae99
Copy link

motae99 commented Jan 5, 2019 via email

@mikeRChambers610
Copy link

Thank you @motae99 . Do I need to import npm i react-native-fetch-blob ? Or is this .blob() a part of the JS and no imports needed for it?

@motae99
Copy link

motae99 commented Jan 7, 2019 via email

@mikechambers610
Copy link

Thanks @motae99 & @mlecoq . I have created #335

I am not sure what is missing from my project but have had no luck with getting my S3Object working through Appsync from React-Native.

@motae99
Copy link

motae99 commented Jan 10, 2019 via email

@motae99
Copy link

motae99 commented Jan 10, 2019 via email

@mikechambers610
Copy link

Hi - I was able to acheive my goal of uploading a picture to S3 and having a url to the S3 object in my dynamoDB. First I am sending the file to S3 via the NPM package 'react-native-aws3'. Once the file is uploaded to the bucket which I have used IAM to grant the cognito role permission, the url is returned as a promise. I then fire off a mutation to dynamodb to create an item in the table with the pic attribute as a String where I am storing the URL. Below code is working :

sendPicS3 = async () => {

 //console.log(this.state.result)
 let file = {
 // `uri` can also be a file system path (i.e. file://)
 uri: this.state.result.uri,
 name: uuidV4()+"profpic.jpg",
 type: "image/png"
}

const config = {
  keyPrefix: "profPic/",
  bucket: AWSConfig.aws_resource_bucket_name,
  region: AWSConfig.aws_project_region,
  accessKey: AWSKeys.acckey,
  secretKey: AWSKeys.seckey,
  successActionStatus:201
}
console.log(config)

RNS3.put(file, config).then(response => {

if (response.status !== 201) {
throw new Error("Failed to upload image to S3");
} else{

console.log(response)
  let id = this.state.backupusername
  //below is the url in the response from the S3 upload
  let profPic = response.body.postResponse.location


 this.props.onUpdateMember({
    id: id,
    profPic: profPic,
  })

  this.setState({
    result: '',
    cancelled: "true",
    profPicChange: "false"
  })

}
});

}

@youneshenniwrites
Copy link

I struggled a lot in implementing file upload using Amplify, AppSync, and S3. Now reading all your answers and working on my own I thought it will be great to share my own solution.

I am using Expo to access the library of the device, RNS3 to put the image in S3, and the AppSync SDK to execute the mutation that store the reference in DynamoDB.

Here are screenshots of my entire code. Keys are from your IAM user.

schema.graphql
schema

MutationCreatePicture.js
mutation

App.js
app

First half of the AddPhoto.js
addphoto-1

Second half of the AddPhoto.js
addphoto-2

keys.js
keys

I tried getting rid of RNS3 by using the Storage API to put the image in S3 (going fully Amplify), but the uploaded image is always broken.

@isocra
Copy link

isocra commented Mar 14, 2019

I'm trying to do a similar thing with Expo and react native and can't get the upload to S3 working correctly. I'm storing the photos in a photos subdirectory, so my local url is given by

  const imageUri = `${FileSystem.documentDirectory}/photos/${filename}`;

Then exactly as described above, I've got

  const imageData = await fetch(imageUri);
  const blobData = await imageData.blob();
  const variables = {
    key: filename,
    region: 'eu-west-2',
    bucket: BUCKET_NAME,
    mimeType: 'image/jpeg',
    localUri: blobData
  }

And that's then passed to the mutation.

Following its path through the debugger, it goes all the way through to the call in node_modules/aws-appsync/lib/link/complex-object-link-uploader.native.js line 19. Here the Body is of this form:

{
  _data: {
    blobId: "5deabc19-2d0e-42f1-9e3c-1667f5a56e7e", 
    offset: 0, 
    size: 0, 
    type: "", 
    lastModified: 1552561833865
  }
}

What's surprising is if I test blobData instanceof Blob up at the beginning, it returns true, but when I try Body instanceof Blob down in complex-object-link-uploader.native.js, then it returns false!!

It feels like my very close, but I just can't see how to make it work... I've tried other approaches like turning it into base64 and putting the string in to localUri, but then the file stored in S3 simply has the base64 encoded version of the image, it doesn't decode it back to an image.

What am I doing wrong?

@youneshenniwrites
Copy link

I'm trying to do a similar thing with Expo and react native and can't get the upload to S3 working correctly. I'm storing the photos in a photos subdirectory, so my local url is given by

  const imageUri = `${FileSystem.documentDirectory}/photos/${filename}`;

Then exactly as described above, I've got

  const imageData = await fetch(imageUri);
  const blobData = await imageData.blob();
  const variables = {
    key: filename,
    region: 'eu-west-2',
    bucket: BUCKET_NAME,
    mimeType: 'image/jpeg',
    localUri: blobData
  }

And that's then passed to the mutation.

Following its path through the debugger, it goes all the way through to the call in node_modules/aws-appsync/lib/link/complex-object-link-uploader.native.js line 19. Here the Body is of this form:

{
  _data: {
    blobId: "5deabc19-2d0e-42f1-9e3c-1667f5a56e7e", 
    offset: 0, 
    size: 0, 
    type: "", 
    lastModified: 1552561833865
  }
}

What's surprising is if I test blobData instanceof Blob up at the beginning, it returns true, but when I try Body instanceof Blob down in complex-object-link-uploader.native.js, then it returns false!!

It feels like my very close, but I just can't see how to make it work... I've tried other approaches like turning it into base64 and putting the string in to localUri, but then the file stored in S3 simply has the base64 encoded version of the image, it doesn't decode it back to an image.

What am I doing wrong?

Check this component in my repo in here. I use Amplify Storage API to put and get images from S3.

@isocra
Copy link

isocra commented Mar 15, 2019

Hi @jtaylor1989,

Thanks for the link. I tried putting your code into my project and it saves zero-length files, whereas if I run your code exactly as-is, yours works fine. So I think that maybe I'm importing something that is messing stuff up? Or I'm not including something I need? I notice, for example, that you've got rn-fetch-blob in your package.json. That seems to have polyfills for fetch and Blob, but you're not actually using them? I tried just adding them to my project but it didn't make any difference. You're also using expo 30 whereas I'm using 32. Any pointers on where I should look?

@isocra
Copy link

isocra commented Mar 15, 2019

I finally found the problem: for some reason in Expo:32, using fetch() and then .blob() doesn't return you the proper object with all the data (no size and no type).

Luckily, @yonahforst posted a solution here: expo/firebase-storage-upload-example#13 (comment)

Basically I needed to get the blob in a different way, here's my version:

const urlToBlob = (url: string) => {
  return new Promise((resolve, reject) => {
    var xhr = new XMLHttpRequest();
    xhr.onerror = reject;
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        resolve(xhr.response);
      }
    };
    xhr.open('GET', url);
    xhr.responseType = 'blob'; // convert type
    xhr.send();
  });
};

Then my example above becomes:

  const blobData = await urlToBlob(imageUri);
  const variables = {
    key: filename,
    region: 'eu-west-2',
    bucket: BUCKET_NAME,
    mimeType: 'image/jpeg',
    localUri: blobData
  }

With that in place, the upload happens successfully and then the mutation works too.

If anyone is wondering why their file isn't uploading, I recommend putting a breakpoint on line 60 or so of node_modules/aws-appsync/lib/link/complex-object-link.js. This is basically where the observer checks to see whether an upload is needed and if it has all the data it needs.

Thanks again @jtaylor1989 for giving me the clue I needed :-)

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

No branches or pull requests

9 participants