Uploading files to AWS S3 from React Native with a Rails bakend
Code to accompany this post: https://finnian.io/blog/uploading-files-to-s3-react-native-ruby-on-rails/
Code to accompany this post: https://finnian.io/blog/uploading-files-to-s3-react-native-ruby-on-rails/
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.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 | |
} | |
} |
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 | |
}) | |
} |
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 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 |