React Native upload to S3 with presigned URL
Asked Answered
S

8

19

Been trying with no luck to upload an image to S3 from React Native using pre-signed url. Here is my code:

generate pre-signed url in node:

const s3 = new aws.S3();

const s3Params = {
  Bucket: bucket,
  Key: fileName,
  Expires: 60,
  ContentType: 'image/jpeg',  
  ACL: 'public-read'
};

return s3.getSignedUrl('putObject', s3Params);

here is RN request to S3:

var file = {
  uri: game.pictureToSubmitUri,
  type: 'image/jpeg',
  name: 'image.jpg',
};

const xhr = new XMLHttpRequest();
var body = new FormData();
body.append('file', file);
xhr.open('PUT', signedRequest);
xhr.onreadystatechange = () => {
  if(xhr.readyState === 4){
    if(xhr.status === 200){
      alert('Posted!');
    }
    else{
      alert('Could not upload file.');
   }
 }
};
xhr.send(body);

game.pictureToSubmitUri = assets-library://asset/asset.JPG?id=A282A2C5-31C8-489F-9652-7D3BD5A1FAA4&ext=JPG

signedRequest = https://my-bucket.s3-us-west-1.amazonaws.com/8bd2d4b9-3206-4bff-944d-e06f872d8be3?AWSAccessKeyId=AKIAIOLHQY4GAXN26FOQ&Content-Type=image%2Fjpeg&Expires=1465671117&Signature=bkQIp5lgzuYrt2vyl7rqpCXPcps%3D&x-amz-acl=public-read

Error message:

<Code>SignatureDoesNotMatch</Code>
<Message>
The request signature we calculated does not match the signature you provided. Check your key and signing method.
</Message>

I can successfully curl and image to S3 using the generated url, and I seem to be able to successfully post to requestb.in from RN (however I can only see the raw data on requestb.in so not 100% sure the image is properly there).

Based on all this, I've narrowed my issue down to 1) my image is not correctly uploading period, or 2) somehow the way S3 wants my request is different then how it is coming in.

Any help would be muuuuuucchhhh appreciated!

UPDATE

Can successfully post from RN to S3 if body is just text ({'data': 'foo'}). Perhaps AWS does not like mutliform data? How can I send as just a file in RN???

Subtraction answered 11/6, 2016 at 5:38 Comment(5)
Not sure why your signature is invalid. I have almost the same signing code and it works fine. Your empty successful uploads are due to you passing a path as an URI. file:///var/.../4BBAE22E-DADC-4240-A266-8E469C0636B8.jpg should work.Accursed
Does your AWS Secret Key have any trailing forwarding "/"?Nonoccurrence
@DanielBasedow I don't think the signature is invalid. I can curl to upload images using it. I think something is wrong with how my RN request is being formed?Subtraction
@Subtraction Just wondering if this is the best practice? When user on client wants to upload a picture I should send filename to Node.js server to generate presigned url -> send presigned url back to client app -> then client app uploads to presigned url?Skidway
@kayla my understanding is direct uploads from client to S3 is the preferred method yes. Sending files through backend creates unnecessary load on servers. Here are the heroku docs: devcenter.heroku.com/articles/s3-upload-nodeSubtraction
B
19

FormData will create a multipart/form-data request. S3 PUT object needs its request body to be a file.

You just need to send your file in the request body without wrapping it into FormData:

function uploadFile(file, signedRequest, url) {
  const xhr = new XMLHttpRequest();
  xhr.open('PUT', signedRequest);
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      if(xhr.status === 200) {
        alert(url);
      } else {
        alert('Could not upload file.');
      }
    }
  };
  xhr.send(file);
};

See https://devcenter.heroku.com/articles/s3-upload-node for example in a browser. Please also ensure your Content-Type header is matched with the signed URL request.

Broken answered 29/6, 2016 at 14:28 Comment(5)
Can you elaborate on ensuring that the Content-Type header is matched with the signed URL request? I do not quite understand what you mean by this.Moshemoshell
You just need to set xhr.setRequestHeader('Content-Type', fileType)Armentrout
I think S3 PUT requires Content-Length see docs, whereas S3 POST requires a file object see docs.Westbrooks
@edward-samuel, I am wondering how to achieve the same with Fetch instead of XMLHttpRequestReeta
This code worked for me on iOS, but not on Android. I get xhr.status = 0 but nothing is uploaded to S3.Annitaanniversary
W
23

To upload pre-signed S3 URL on both iOS and Android use react-native-blob-util lib

Code snippet:

import RNBlobUtil from 'react-native-blob-util'

const preSignedURL = 'pre-signed url'
const pathToImage = '/path/to/image.jpg' // without file:// scheme at the beginning
const headers = {}

RNBlobUtil.fetch('PUT', preSignedURL, headers, RNBlobUtil.wrap(pathToImage))

Edited 19 Oct 2022 and swapped unsupported RN Fetch Blob for React Native Blob Util package.

Weaponless answered 10/8, 2018 at 21:44 Comment(8)
you're my life saver!Snowfall
@Peter Machowski Is it working normally for large files too?Hesitation
@UmangLoriya I haven't tested it with files other than <10Mb images but I don't see a reason for not working with larger files.Weaponless
Can you explain how's your headers object looks like ? I'm passing a FormData object containing all the needed infos with the presigned URL (bucket, region, key, ...), but then I'm getting a black screen when attempting to upload...Antineutrino
@Antineutrino there are no specific headers needed. Black screen suggest error in some other code.Weaponless
I tried this, but it crashes on iOS, have you experienced this?Lucero
@PeterMachowski Thanks a lot! The file gets uploaded but when I try to download it, it says No Such Key. What could cause this ?Bosanquet
After wasting 3hrs and trying lost of solutions, this was the answer that worked for me!Parent
B
19

FormData will create a multipart/form-data request. S3 PUT object needs its request body to be a file.

You just need to send your file in the request body without wrapping it into FormData:

function uploadFile(file, signedRequest, url) {
  const xhr = new XMLHttpRequest();
  xhr.open('PUT', signedRequest);
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      if(xhr.status === 200) {
        alert(url);
      } else {
        alert('Could not upload file.');
      }
    }
  };
  xhr.send(file);
};

See https://devcenter.heroku.com/articles/s3-upload-node for example in a browser. Please also ensure your Content-Type header is matched with the signed URL request.

Broken answered 29/6, 2016 at 14:28 Comment(5)
Can you elaborate on ensuring that the Content-Type header is matched with the signed URL request? I do not quite understand what you mean by this.Moshemoshell
You just need to set xhr.setRequestHeader('Content-Type', fileType)Armentrout
I think S3 PUT requires Content-Length see docs, whereas S3 POST requires a file object see docs.Westbrooks
@edward-samuel, I am wondering how to achieve the same with Fetch instead of XMLHttpRequestReeta
This code worked for me on iOS, but not on Android. I get xhr.status = 0 but nothing is uploaded to S3.Annitaanniversary
R
6
"rn-fetch-blob": 0.12.0,
"react-native": 0.61.5

This code works for both Android & iOS

const response = await RNFetchBlob.fetch(
  'PUT',
  presignedUrl,
  {
    'Content-Type': undefined
  },
  RNFetchBlob.wrap(file.path.replace('file://', '')),
)

Note {'Content-Type': undefined} is needed for iOS

Rountree answered 3/11, 2020 at 14:28 Comment(1)
Using RNFetchBlob is simplest solution for me. Also specifying Content-Type = undefined is of course important.Manned
T
0

sorry if none worked for any body. took me 5 days to get this to work . 5 crazy days of no result until my sleepy eyes turned green after little nap. Guess i had a sweet dream that brought the idea. so quickly say u have an end point on ur server to generate the sign url for the request from react native end or from react side or any web frontier. i would be doing this for both react native and react(can serve for html pages and angular pages).

WEB APPROACH

UPLOAD IMAGE TO S3 BUCKET PRESIGNED URI

/*
      Function to carry out the actual PUT request to S3 using the signed request from the app.
    */
    function uploadFile(file, signedRequest, url){
     // document.getElementById('preview').src = url; // THE PREVIEW PORTION
        //    document.getElementById('avatar-url').value = url; //
      const xhr = new XMLHttpRequest();
      xhr.open('PUT', signedRequest);
      xhr.onreadystatechange = () => {
        if(xhr.readyState === 4){
          if(xhr.status === 200){
            document.getElementById('preview').src = url;
           // document.getElementById('avatar-url').value = url;
          }
          else{
            alert('Could not upload file.');
          }
        }
      };
      xhr.send(file);
    }

    /*
      Function to get the temporary signed request from the app.
      If request successful, continue to upload the file using this signed
      request.
    */
    function getSignedRequest(file){
      const xhr = new XMLHttpRequest();

      xhr.open('GET', 'http://localhost:1234'+`/sign-s3?file-name=${file.name}&file-type=${file.type}`);
        xhr.setRequestHeader('Access-Control-Allow-Headers', '*');
    xhr.setRequestHeader('Content-type', 'application/json');
    xhr.setRequestHeader('Access-Control-Allow-Origin', '*');
      xhr.onreadystatechange = () => {
        if(xhr.readyState === 4){
          if(xhr.status === 200){
            const response = JSON.parse(xhr.responseText);
            uploadFile(file, response.signedRequest, response.url);
          }
          else{
            alert('Could not get signed URL.');
          }
        }
      };
      xhr.send();
    }

    /*
     Function called when file input updated. If there is a file selected, then
     start upload procedure by asking for a signed request from the app.
    */
    function initUpload(){
      const files = document.getElementById('file-input').files;
      const file = files[0];
      if(file == null){
        return alert('No file selected.');
      }
      getSignedRequest(file);
    }

    /*
     Bind listeners when the page loads.
    */


   //check if user is actually on the profile page
//just ensure that the id profile page exist  on your html
  if (document.getElementById('profile-page')) {
    document.addEventListener('DOMContentLoaded',() => {

      ///here is ur upload trigger bttn effect

        document.getElementById('file-input').onchange = initUpload;
    });

  }



Teetotum answered 3/6, 2020 at 3:31 Comment(0)
T
0

FOR REACT NATIVE I WILL NOT BE USING ANY 3RD PARTY LIBS.

i have my pick image function that picks the image and upload using xhr

const pickImage = async () => {
    let result = await ImagePicker.launchImageLibraryAsync({
     // mediaTypes: ImagePicker.MediaTypeOptions.All,
      allowsEditing: true,
      aspect: [4, 3],
      quality: 1,
      base64:true
    });

    console.log(result);






    if (!result.cancelled) {
     // setImage(result.uri);
      let base64Img = `data:image/jpg;base64,${result.uri}`;




       // ImagePicker saves the taken photo to disk and returns a local URI to it
  let localUri = result.uri;
  let filename = localUri.split('/').pop();

  // Infer the type of the image
  let match = /\.(\w+)$/.exec(filename);
  let type = match ? `image/${match[1]}` : `image`;

  // Upload the image using the fetch and FormData APIs
  let formData = new FormData();
  // Assume "photo" is the name of the form field the server expects
  formData.append('file', { uri: base64Img, name: filename, type });

  const xhr = new XMLHttpRequest();


  xhr.open('GET', ENVIRONMENTS.CLIENT_API+`/sign-s3?file-name=${filename}&file-type=${type}`);
  xhr.setRequestHeader('Access-Control-Allow-Headers', '*');
xhr.setRequestHeader('Content-type', 'application/json');
// xhr.setRequestHeader('Content-type', 'multipart/form-data');
xhr.setRequestHeader('Access-Control-Allow-Origin', '*');
 xhr.setRequestHeader('X-Amz-ACL', 'public-read') //added
xhr.setRequestHeader('Content-Type', type) //added
xhr.onreadystatechange = () => {
  if(xhr.readyState === 4){
    if(xhr.status === 200){
      const response = JSON.parse(xhr.responseText);
      alert(JSON.stringify( response.signedRequest, response.url))
      // uploadFile(file, response.signedRequest, response.url);
      // this.setState({imagename:file.name})
      const xhr2 = new XMLHttpRequest();

            xhr2.open('PUT', response.signedRequest);
            xhr2.setRequestHeader('Access-Control-Allow-Headers', '*');
            xhr2.setRequestHeader('Content-type', 'application/json');
            // xhr2.setRequestHeader('Content-type', 'multipart/form-data');
            xhr2.setRequestHeader('Access-Control-Allow-Origin', '*');
            //  xhr2.setRequestHeader('X-Amz-ACL', 'public-read') //added
            xhr2.setRequestHeader('Content-Type', type) //added
            xhr2.onreadystatechange = () => {
              if(xhr2.readyState === 4){
                if(xhr2.status === 200){

                  alert("successful upload ")
                }
                else{
                  // alert('Could not upload file.');
                  var error = new Error(xhr.responseText)
                  error.code = xhr.status;
                  for (var key in response) error[key] = response[key]
                  alert(error)
                }
              }
            };
            xhr2.send( result.base64)
    }
    else{
      alert('Could not get signed URL.');
    }
  }
};
xhr.send();








    }


  };






then some where in the render method

<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button title="Pick an image from camera roll" onPress={pickImage} />
      {image && <Image source={{ uri: image }} style={{ width: 200, height: 200 }} />}
    </View>


hope it helps any one who doesnt want sleepless nights like me.
Teetotum answered 3/6, 2020 at 3:38 Comment(0)
B
0
import React from 'react'
import { Button, SafeAreaView } from 'react-native'
import { launchImageLibrary } from 'react-native-image-picker'

const Home = () => {

  const getImageFromLibrary = async () => {
    const result = await launchImageLibrary()

    const { type, uri } = result.assets[0]

    const blob = await new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest()
      xhr.onload = function () {
        resolve(xhr.response)
      }
      xhr.onerror = function () {
        reject(new TypeError('Network request failed'))
      }
      xhr.responseType = 'blob'
      xhr.open('GET', uri, true)
      xhr.send(null)
    })

    // Send your blob off to the presigned url
    const res = await axios.put(presignedUrl, blob)
  }

  return (
    <SafeAreaView>
      <Button onPress={getImageFromLibrary} title="Get from library" />    
    </SafeAreaView>
  )
}

export default Home

Your BE that creates the pre-signed url can look something like this (pseudo code):

const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3')

const BUCKET_NAME = process.env.BUCKET_NAME
const REGION = process.env.AWS_REGION

const s3Client = new S3Client({
  region: REGION
})

const body = JSON.parse(request.body)
const { type } = body

const uniqueName = uuidv4()
const date = moment().format('MMDDYYYY')
const fileName = `${uniqueName}-${date}`

const params = {
  Bucket: BUCKET_NAME,
  Key: fileName,
  ContentType: type
}

try {
  const command = new PutObjectCommand(params)
  const signedUrl = await getSignedUrl(s3Client, command, {
    expiresIn: 60
  })

  response.send({ url: signedUrl, fileName })
} catch (err) {
  console.log('ERROR putPresignedUrl : ', err)
  response.send(err)
}

I am using aws-sdk v3 which is nice because the packages are smaller. I create a filename on the BE and send it to the FE. For the params, you don't need anything listed then those 3. Also, I never did anything with CORS and my bucket is completely private. Again, the BE code is pseudo code ish so you will need to edit a few spots.

Lastly, trying to use the native fetch doesn't work. It's not the same fetch you use in React. Use XHR request like I showed else you cannot create a blob.

Ballot answered 15/6, 2022 at 0:49 Comment(0)
I
0

First, install two libraries, then the image convert into base64 after that arrayBuffer, then upload it

import RNFS from 'react-native-fs';
import {decode} from 'base64-arraybuffer';

 try {
        RNFS.readFile(fileUri, 'base64').then(data => {
          const arrayBuffer = decode(data);
          axios
            .put(sThreeApiUrl.signedUrl, arrayBuffer, {
              headers: {
                'Content-Type': 'image/jpeg',
                'Content-Encoding': 'base64',
              },
            })
            .then(res => {
              if (res.status == 200) {
               console.log('image is uploaded successfully');              
              }
            });
        });
      } catch (error) {
        console.log('this is error', error);              }
Impostor answered 15/6, 2022 at 3:38 Comment(0)
A
0

Android status code 0 issue is fixed by putting setrequestheader before onreadystatechange

const xhr = new XMLHttpRequest();
xhr.open('PUT', presignedUrl);
xhr.setRequestHeader('Content-Type', photo.type)
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      alert("file uploaded")
    } else {
      alert('Could not upload file.');
    }
  }
};
xhr.send(file);
Abyss answered 19/8, 2023 at 9:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.