Rails ActiveStorage Direct Upload on React Native
Allan L贸pez
Posted on May 14, 2023
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:
- Get the signed upload URL
- Upload the file
- 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
},
};
}
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,
});
}
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
});
}
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
}
}
})
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,
},
},
});
}
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.
Posted on May 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.