UPLOADING FILES FROM REACT NATIVE TO AN S3 PRESIGNED URL.
Tags: typescript, reactnative, aws, expo
Published: 2021-07-29 18:57:15

Been off here for a while because I have mostly been building  ..scratch that..its been more of fighting with really stubborn bugs🤦🏽‍♂️. I am not done yet but I have decided to take sometime out to share my learnings and bug fixes through a series of articles. I honestly didn't expect uploading documents to an aws pre-signed url to be as stressful as it was for me. Luckily, I got that solved and here is what I did(errmm yeah.. i checked stack overflow and it didn't work for me...for those of you already thinking that 🙄).

This article like the rest of my previous articles, is to serve as a guide to anyone who is just like me, having tried the common methods, you may have found a solution. Now, I must point out that I am a noob as regards what the backend code/architecture looks like,  as I was the frontend guy on this.

I will be working with expo and typescript but you can also use this with react-native and javascript too.

###1) INSTALL THE NEEDED DEPENDENCIES
Firstly, the following packages need to be installed
- `expo install expo-document-picker`
- `npm i expo-file-system`
- `npm i buffer`


###2) PICKING OUR FILE
Next we will create our file picker function, which when called will open up a document picker and try to get the image data. If that is successful, it will save it as a state value.

```typescript
import * as DocumentPicker from "expo-document-picker";

interface FileInterface {
  type: "success";
  name: string;
  size: number;
  uri: string;
  lastModified?: number | undefined;
  file?: File | undefined;
  output?: FileList | null | undefined;
}

//--> called when the file is picked
  const pickFile = async () => {
    try {
    //--> Pick file(PDF/IMG/OTHER)
    const file = await DocumentPicker.getDocumentAsync({
      type: "*/*",
      copyToCacheDirectory: true,
    });
      setFile(pickFile);
      //this is stored as a state value and this value will 
      //then be set in a useEffect dependency array to trigger 
      //the function below when the file state is updated.

     }
    catch(error){
      //toast error
    }
  };
```



###3) CONVERTING TO A BLOB
The next step is generating a blob file using the fetch api. 

According to **MDN**, The Blob object represents a blob, which is a file-like object of immutable, raw data; they can be read as text or binary data, or converted into a ReadableStream so its methods can be used for processing the data.
Blobs can represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system. 😴💤🥱

Okay back to Code🤓

```typescript
const getBlob = async (fileUri: string) => {
 try {
  const resp = await fetch(fileUri);
  const imageBody = await resp.blob();
  return imageBody;
 } catch (err){
  //toast err
};
```


###4)GETTING YOUR SIGNED URL

```typescript
const signedUrl = await axiosClient.post(`${ENV.GENERATED_SIGNED_URL}`, payload);
 if (signedUrl == null) {
   //Toast error
   return;
  } else {
const { url, uploadID, fileName } = signedUrl.data.data;
```


###5)CONVERTING BLOB URI DATA TO BASE 64

```typescript
//Converting the uri obtained from the blob object to base64
  const base64 = await FileSystem.readAsStringAsync(uri, {
  encoding: FileSystem.EncodingType.Base64});
```

###6)CONVERTING BASE 64 DATA TO A BUFFER

```typescript
//Converting the base64 to a buffer
  const buffer = Buffer.from(base64, "base64");
```

###7) UPLOADING TO S3
 The final step is the actual uploading of the buffer data to the pre-signed URL.

```typescript
  const uploadFile = await axios({
    method: "PUT",
    url: url,
    data: buffer,
    headers: { "Content-Type": fileType ?? "image/jpeg" },
  });
```

###8) PUTTING IT ALL TOGETHER
```typescript
import {useState, useEffect} from "react";
import axios from "axios";
import * as DocumentPicker from "expo-document-picker";
import axiosClient from "utils/axiosClient";
import * as FileSystem from "expo-file-system";
import {Buffer} from "buffer";

interface FileInterface {
  type: "success";
  name: string;
  size: number;
  uri: string;
  lastModified?: number | undefined;
  file?: File | undefined;
  output?: FileList | null | undefined;
}

interface DummySignedURLPayloadInterface {
  [key: string]: string;
}

const signedURLpayload: DummySignedURLPayloadInterface = {
  name: "mazi_juls",
  twitterhandle: "@ajulibe",
  id: "007"
};

export const useUploadToS3 = () => {
  const [file, setFile] = useState<FileInterface>();
  const [res, setRes] = useState("👀");

  useEffect(() => {
    if (typeof file === "undefined") {
      return;
    } else {
      uploadFileToS3(signedURLpayload, file);
    }
  }, [file]);

  async function getBlob(fileUri: string) {
    const resp = await fetch(fileUri);
    const imageBody = await resp.blob();
    return imageBody;
  }

  //--> called when the file is picked
  const pickFile = async () => {
    //--> Pick file(PDF/IMG/OTHER)
    const file = await DocumentPicker.getDocumentAsync({
      type: "*/*",
      copyToCacheDirectory: true,
    });

    if (file.type !== "success") {
      //--> Toast error
      return null;
    } else {
      setFile(file);
      //this is stored as a state value which is then added to the useEffect dependency array
      //to trigger the uploadFileToS3 function
    }
  };

  const uploadFileToS3 = async (
    signedURLpayload: DummySignedURLPayloadInterface,
    file: FileInterface,
  ) => {
    try {
      //--> Pick file(PDF/IMG/OTHER)
      if (file.type !== "success") {
        //--> Toast error
        return null;
      } else {
        let {uri} = file;
        const fileBody = await getBlob(uri);
        const fileType = fileBody["type"];
        if (fileBody == null) {
          //--> Toast error
          return null;
        } else {
          //--> Get signed URL
          const signedUrl = await axiosClient.post(
            "Upload/Generates3URL",
            signedURLpayload,
          );
          if (signedUrl == null) {
            throw new Error("😩😩");
          } else {
            //--> destructuring out the signed URL from the response
            const {url} = signedUrl.data.data;
            const base64 = await FileSystem.readAsStringAsync(uri, {
              encoding: FileSystem.EncodingType.Base64,
            });
            const buffer = Buffer.from(base64, "base64");
            const uploadFile = await axios({
              method: "PUT",
              url: url,
              data: buffer,
              headers: {"Content-Type": fileType ?? "image/jpeg"},
            });
            if (uploadFile.status === 200) {
              console.log("🥳🥳");
              setRes("🥳🥳");
            } else {
              console.log("😩😩");
              throw new Error("😩😩");
            }
          }
        }
      }
    } catch (error) {
      setRes("😩😩")
      console.log("😩😩");
      //--> toast error
    }
  };

  return {
    res,
    pickFile,
  };
};

//--> This can be used in your component like a hook
import React from "react";
import {View,Text,TouchableOpacity} from "react-native";
import {useUploadToS3} from "./yourHooksFolder";

const MazisFuncCmp: React.FC = () => {
  const {res, pickFile} = useUploadToS3();

  return (
    <View>
      <Text>Hi There...</Text>
      <Text>{res}</Text>
      <TouchableOpacity type="button" onPress={pickFile}>
        <Text>Click me to test 👨🏽‍🏫 </Text>
      </TouchableOpacity>
    </View>
  );
};

export default MazisFuncCmp;

```
###SUMMARY:
After a file has been picked successfully, its value is stored as state which then triggers the useEffect hook to run again. At this point, the value of file is not undefined so the else block is triggered. This calls the uploadFileToS3 function. The uploadFileToS3 function is made up of all the code snippets shown earlier all wrapped as an async operation.✌🏽