Rails ActiveStorage Direct Upload on React Native
Tags: reactnative, frontend, mobile, rails
Published: 2023-05-14 18:12:50

Recently, I needed to integrate direct upload functionality into a React Native application. After spending some time researching Internet solutions for Rails ActiveStorage Direct Uploads (which are rather sparse), I found out that it can be done from within the app and I am sharing my experience here.

On this post I will only be showing the front-end part, for more information about backend work you can follow [this guide](https://blog.commutatus.com/understanding-rails-activestorage-direct-uploads-a4aeca7eccf).

I will show how to upload images from expo image picker, but you can use whatever other library for this.

My first thought was to use the Rails Active Storage Javascript library. This would be simple and ideal, but as I found out it doesn't work well when used on React Native because this package uses DOM Form APIs which are not supported in an app environment.

Luckily  I found a [thread](https://github.com/rails/rails/issues/32208#issuecomment-383737803) with some instructions on how to make it work on React Native.

In order to make the Direct upload we need to follow 3 steps:

1. Get the signed upload URL
2. Upload the file
3. Update the Rails model with the signed_id

**1. Get the signed upload URL**

Once we have called ImagePicker.launchImageLibraryAsync()  and have a successful response we are going to use the ImagePicker.ImagePickerAsset result file.

We will need to get some info of the file for the first request, for that we are going to use the expo-file-system library.
(the meta data you need will depend on your BE implementation)

```typescript
import { Buffer } from 'buffer';
import * as FileSystem from 'expo-file-system';
import * as ImagePicker from 'expo-image-picker';

async function uploadFile(file: ImagePicker.ImagePickerAsset){
 const meta = await FileSystem.getInfoAsync(file.uri, { md5: true });
  const md5 = meta.md5;
  const checksum = Buffer.from(md5, 'hex').toString('base64');
  const fileExtension = file.uri.split('.').pop();

 const blobInfo = {
    blob: {
      filename:`fileName.${fileExtension}`,
      byteSize: meta.size,
      checksum,
      contentType: "image/jpeg", //this depends on the type of your image
    },
  };
}
```

Once we have the data we need, we will send our first POST request to our BE to get the signed upload url.

> The request can look something like this, but will vary depending your implementation.

```typescript
import { Buffer } from 'buffer';
import * as FileSystem from 'expo-file-system';
import * as ImagePicker from 'expo-image-picker';

async function uploadFile(file: ImagePicker.ImagePickerAsset) {
  const meta = await FileSystem.getInfoAsync(file.uri, { md5: true });
  const md5 = meta.md5;
  const checksum = Buffer.from(md5, 'hex').toString('base64');
  const fileExtension = file.uri.split('.').pop();

  const blobInfo = {
    blob: {
      filename: `fileName.${fileExtension}`,
      byteSize: meta.size,
      checksum,
      contentType: 'image/jpeg',
    },
  };
  const { data: signedResponse } = await fetch('/rails/active_storage/direct_uploads', {
    method: 'POST',
    body: blobInfo,
  });
}
```

We will be using the direct upload url and direct upload headers from the response.

**2. Upload the file**

Once we have the signed direct url we are going to upload the file as a blob.

```typescript
async function uploadFile(file: ImagePicker.ImagePickerAsset){
.....
.....
  //We need to clean the URI on IOS in order to have the correct route for the file
  const fileUri =
      Platform.OS === 'ios' ? file.uri.replace('file://', '/') : file.uri;
    const fileResponse = await fetch(fileUri);
    const blob = await fileResponse.blob();
    //signed url from previous POST
    await fetch(signedResponse.direct_upload.url, {
      method: 'PUT',
      // Important to pass the same headers we got on the first request, if not passed you could have authorization errors.
      headers: signedResponse.direct_upload.headers, 
      body: blob, // pass the blob directly to the body
    });

}
```
 

**3. Update the Rails model with the signed_id**

Finally send the signed id to the backend in order to be saved.

```typescript
fetch(/* user_url */, {
  method: 'PUT',
  body: {
    user: {
      image: response.signed_id
    }
  }
})
```
**Final code**

```typescript
import { Buffer } from 'buffer';
import * as FileSystem from 'expo-file-system';
import * as ImagePicker from 'expo-image-picker';

async function uploadFile(file: ImagePicker.ImagePickerAsset) {
  const meta = await FileSystem.getInfoAsync(file.uri, { md5: true });
  const md5 = meta.md5;
  const checksum = Buffer.from(md5, 'hex').toString('base64');
  const fileExtension = file.uri.split('.').pop();

  const blobInfo = {
    blob: {
      filename: `fileName.${fileExtension}`,
      byteSize: meta.size,
      checksum,
      contentType: 'image/jpeg',
    },
  };

  const {data: signedResponse} = await fetch('/rails/active_storage/direct_uploads', {
    method: 'POST',
    body: blobInfo,
  });

  const fileUri =
    Platform.OS === 'ios' ? file.uri.replace('file://', '/') : file.uri;
  const fileResponse = await fetch(fileUri);
  const blob = await fileResponse.blob();

  await fetch(signedResponse.direct_upload.url, {
    method: 'PUT',
    headers: signedResponse.direct_upload.headers,
    body: blob,
  });

  await fetch(/* user url */, {
    method: 'PUT',
    body: {
      user: {
        image: signedResponse.signed_id,
      },
    },
  });
}

```
 

**In summary**: Create a blob, which gives you a signed upload url. PUT the file to that signed URL. Then update the rails model to assign your signed url to the attachment relation.

Thanks to [@cbothner](https://github.com/cbothner) and [@jbschrades](https://github.com/jbschrades) for pointing us to the right direction.