Skip to content

Instantly share code, notes, and snippets.

@developius
Last active May 23, 2022 09:37
Show Gist options
  • Save developius/1fa35f2192b886dfce4e7f4eaed8b923 to your computer and use it in GitHub Desktop.
Save developius/1fa35f2192b886dfce4e7f4eaed8b923 to your computer and use it in GitHub Desktop.
React Native + Ruby on Rails + AWS S3 Presigned PUT
import { Image } from "react-native-image-crop-picker"
interface IReport {
field_one: string
field_two: string
field_three: string
attachments: string[]
}
interface IDraftAttachment {
signature: {
url: 'https://bucket.s3.region.amazonaws.com/etc',
headers: {
"Content-Type": "image/jpeg",
"Content-MD5": "3Tbhfs6EB0ukAPTziowN0A=="
},
signed_id: 'signedidoftheblob'
},
file: Image
}
export class Api {
async uploadAttachment(
attachment: IDraftAttachment,
onProgress: (progress: number, total: number) => void
): Promise<void> {
console.log('Uploading attachment to:', attachment.signature)
const attachmentUrl = await new Promise<string>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('PUT', attachment.signature.url, true);
Object.keys(attachment.signature.headers).forEach(key => {
xhr.setRequestHeader(key, attachment.signature.headers[key])
})
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) return
if (xhr.status === 200) return resolve()
reject(xhr.status)
}
xhr.upload.onprogress = e => {
onProgress(e.loaded, e.total)
}
xhr.send({
uri: attachment.file.path,
type: attachment.file.mime,
name: attachment.file.filename || Date.now().toString()
});
})
}
async createReport(report: IReport): Promise<number> {
const result = await axios.post('/api/reports', report)
return result.data.id // ID of the newly-created report
}
}
// source: @f-g-p (https://github.com/f-g-p)
export type PresignedUploadResponse = {
data: {
url: string;
headers: {
'Content-MD5': string;
'Content-Disposition': string;
};
signed_id: string;
};
};
async uploadAsync(
presignedData: PresignedUploadResponse,
file: ImageResult,
onProgress: (progress: number, total: number) => void
): Promise<string> {
const base64String = await FileSystem.readAsStringAsync(file.uri, { encoding: 'base64' });
const buffer = Buffer.from(base64String, 'base64');
const headers = { ...presignedData.data.headers, 'content-type': '' };
await axios.put(presignedData.data.url, buffer, {
headers,
onUploadProgress: (progressEvent) => onProgress(progressEvent.loaded, progressEvent.total),
});
return presignedData.data.signed_id;
},
import { Buffer } from 'buffer'
import RNFS from 'react-native-fs'
import ImagePicker, {
Image as ImagePickerImage,
Options as ImagePickerOptions,
} from "react-native-image-crop-picker"
// https://github.com/ivpusic/react-native-image-crop-picker#request-object
const options: ImagePickerOptions = {
cropping: false,
multiple: true,
sortOrder: "asc",
maxFiles: 10,
// we don't need the base64 here as we upload the file directly
// from the FS rather than a base64-encoded string of it
includeBase64: false
}
// call openImageLibrary from a <Button> or similar
const openImageLibrary = async () => {
const results = (await ImagePicker.openPicker(options)) as ImagePickerImage[]
results.forEach(async r => {
const hex = await RNFS.hash(r.path, 'md5')
const base64 = Buffer.from(hex, 'hex').toString('base64')
// save base64 somewhere with the attachment for use when retrieving the presigned url
})
}
// If you're using expo, this code will apply most to you.
async function retrieveDirectUpload(uri: string) {
const info = await FileSystem.getInfoAsync(uri, {
size: true,
md5: true,
});
// the expo docs for getInfoAsync don't specify that the md5 is in hex, which took a while to figure out
const base64 = Buffer.from(info.md5, 'hex').toString('base64');
const splitUri = info.uri.split('/');
const filename = splitUri[splitUri.length - 1];
// call the Rails backend however you like to get the url & headers to upload to
const result = await retrieveDirectUploadUrl({
filename,
byteSize: info.size,
contentType: 'image/jpeg', // this must match the value below
checksum: base64,
});
return result;
}
async function uploadFile(uri: string) {
const info = retrieveDirectUpload(uri);
const headers = {
'Content-Type': 'image/jpeg', // this must match the content-type of the file you're uploading
...JSON.parse(info.headers), // these are the headers coming back from `service_headers_for_direct_upload`
};
const task = FileSystem.createUploadTask(
info.url,
uri,
{
headers,
httpMethod: 'PUT',
},
data =>
console.log(data.totalBytesExpectedToSend, data.totalBytesExpectedToSend),
);
return task.uploadAsync();
}
class Api::PresignedUploadController < Api::BaseController
# POST /api/presigned-upload
def create
create_blob
render_success(
data: {
url: @blob.service_url_for_direct_upload(expires_in: 30.minutes),
headers: @blob.service_headers_for_direct_upload,
signed_id: @blob.signed_id
}
)
end
private
def create_blob
@blob = ActiveStorage::Blob.create_before_direct_upload!(
filename: blob_params[:filename],
byte_size: blob_params[:byte_size],
checksum: blob_params[:checksum],
content_type: blob_params[:content_type]
)
end
def blob_params
params.require(:file).permit(:filename, :byte_size, :checksum, :content_type)
end
end
class Report < ApplicationRecord
has_many_attached :attachments
# etc
end
class Api::ReportsController < Api::BaseController
# POST /api/reports
def create
@report = Report.new(report_params)
if @report.save
render_success(data: @report)
else
render_error(400, object: @report)
end
end
private
def report_params
params.permit(
:field_one, :field_two, :field_three,
attachments: [] # this is where the signed_ids will end up
)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment