Rails ActiveStorage Direct Upload on React Native

allanloji

Allan L贸pez

Posted on May 14, 2023

Rails ActiveStorage Direct Upload on React Native

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.

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 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)

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
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

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.

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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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.

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
    });

}
Enter fullscreen mode Exit fullscreen mode

3. Update the Rails model with the signed_id

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

fetch(/* user_url */, {
  method: 'PUT',
  body: {
    user: {
      image: response.signed_id
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Final code

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,
      },
    },
  });
}

Enter fullscreen mode Exit fullscreen mode

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 and @jbschrades for pointing us to the right direction.

馃挅 馃挭 馃檯 馃毄
allanloji
Allan L贸pez

Posted on May 14, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related