- はじめに
- AWS Amplifyでアップロードする場合
- なぜAWS Amplifyを使わずにアップロードしたいのか
- さっそくReact Nativeからアップロードしてみる
- 実行
- まとめ
- ソースコード
はじめに
AWSのS3にファイルをアップロードするにあたり、今だとAWS Amplifyを使ったアップロードを進められることが多いと思う。
だが、今回はAWS Amplifyを使わずにReact NativeでS3にファイルアップロードしてみようという話。
AWS Amplifyでアップロードする場合
AWS Amplify Libraryを使ってアップロードすることのメリットは一応ある。それはマルチパートアップロードの実装がとても簡単にできるということだ。というよりも、そもそもマルチパートでアップロードをするかどうかを意識する必要がほとんどない。
AWS Amplify Libraryに用意されているStorage.put()
を使えば必要に応じてチャンクに分割して送ってくれるのだ。しかもResume処理なんかも自前で実装しなくてもいい。
なぜAWS Amplifyを使わずにアップロードしたいのか
先にあげたように普通に考えればAWS Amplifyを使ってS3にアップロードする処理を書いたほうが圧倒的に楽だ。いや、楽だと思っていた。
これに関しては半分正解だ。確かにあまり深く考えずともマルチパートでアップロードしてくれるし、一時停止や再開などの実装をする必要もない。
だが、自分の場合は大きなファイルのアップロードで問題が発生した。詳細は割愛するがAWS AmplifyのStorage.put()
で大きいサイズのファイルを扱おうとするとOut of Memoryが発生してしまう。この問題はIssueもあがっているみたいで詳細も調べたものの、今回の本題ではないので細かく説明などはしない。
ファイルサイズを小さくすれば問題ないのでは?とか分割したらどうか?とかも思いついたしやってみたが、一回の処理は問題ないものの連続すると結局AmplifyのStorage.put
の実装方法の兼ね合いでそのうちOOMで死ぬことになった。
ちなみにReact Nativeではfetch
にも少し問題があって160MB以上くらいのファイルを扱おうとするとこちらもエラーになる。これもIssueあがってるので興味ある人はググってもらうといい。
というわけでAWS Amplifyを使わずにアップロードをする必要に迫られたというのが正しい。
さっそくReact Nativeからアップロードしてみる
AWS Amplifyを使わないということはつまりAWS SDKを使って自前で実装するということだ。
React Nativeの場合はiOSやAndroid向けのSDKではなく、普通にJavaScript向けのSDKを利用すればいい。とりあえず今回はマルチパートではなくシンプルに動画をアップロードする処理を試してみる。
前提としてAWSアカウントはもちろん、アップロード対象のS3バケット、認証のためのCognito Identity Pool、それに紐付けるIAMロールを事前に用意しておく必要がある。このあたりは本題ではないので割愛。
さて、適当なReact Nativeなアプリを用意したら、まずはこのあたりのパッケージをインストールする。
AWS関連に加えて、アップロードする動画をiOSでいうところのカメラロールから選択するためのピッカーやファイル読み込み周りで必要になるものも入れている。ピッカーはreact-native-image-picker
、ファイルシステムを扱うのにreact-native-fs
、base64で読み込んだファイルをデコードしたりするためのバッファ関連はbuffer
を使ってもいいが今回はbase64-arraybuffer
を使うことにした。
yarn add @aws-sdk/client-cognito-identity yarn add @aws-sdk/credential-provider-cognito-identity yarn add @aws-sdk/client-s3 yarn add react-native-fs yarn add react-native-image-picker yarn add base64-arraybuffer
で、肝心の処理はこうだ。S3のクライアント接続周りはどこかで済ませておく。
const region = 'ap-northeast-1'; const bucket = 'sample-bucket'; const poolId = 'ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; const client = new S3Client({ region, credentials: fromCognitoIdentityPool({ client: new CognitoIdentityClient({region}), identityPoolId: poolId, }), });
リージョンやバケット名、Cognito Identity PoolのIDなんかは自分のもので。
次に、react-native-image-picker
を使ってアップロードするビデオを選択する部分。ちょっと前のサンプルとかだとlaunchImageLibrary()
ではなく、showImagePicker()
を使ったものが多いが最近のバージョンからはこのAPIは削除されていて、今だとlaunchImageLibrary()
を使うようになっているので注意。
const chooseMovie = () => { let options = { mediaType: 'video', }; launchImageLibrary(options, response => { if (response.didCancel) { console.log('Cancelled'); } else if (response.error) { console.log('Error: ', response.error); } else { const file = { uri: response.assets[0].uri, name: response.assets[0].fileName, type: 'video/quicktime', size: response.assets[0].fileSize, }; uploadVideo(file); } }); };
で、この中から呼び出しているuploadVideo()
がこちら。
const uploadVideo = async file => { let contentType = 'video/quicktime'; let contentDeposition = 'inline;filename="' + file.name + '"'; const fileData = await RNFS.readFile(file.uri, 'base64'); const buffer = decode(fileData); const params = { Bucket: bucket, Key: file.name, Body: buffer, ContentDisposition: contentDeposition, ContentType: contentType, }; try { const response = await client.send(new PutObjectCommand(params)); console.log('success'); } catch (error) { console.log({error}); } };
やっていることはシンプル。
react-native-fs
のreadFile
で選択したビデオファイルをbase64で読み込み、- それをデコードしてArrayBufferに格納する
- デコードしたオブジェクトを含むパラメータを設定して、
PutObjectCommend
をnewしてsend
に渡す
これだけでひとまずできあがり。 上記を含むソースコード全文は文末に。
実行
ここまで来たらあとは実行するだけ。僕の場合はiOSのシミュレータで試す。プロジェクトのiOSフォルダに移動していつものやつを実行するだけだ。
pod install yarn ios
こんな感じで動く。なお、この例ではアップロード後のメッセージとかを画面上でハンドリングしていないので成否はコンソールのログを見る必要がある。
子どもが写ってるが気にしないで欲しい。ちなみにこのテストをしているときにシミュレータもiCloudでカメラロールの同期が可能なことを知った。
まとめ
という感じでマルチパートでなければAmplifyのStorage.put()
を使うのと大差ない。Storage.put()
も結局ファイル読み込み周りは自分で書かないといけないので。
なので、次はマルチパートを実装してみよう。どのくらい面倒なのか。
ソースコード
ソースコード全文はこちら。今回は適当に試すだけなのでApp.jsxにベタ書きしている。
import React from 'react'; import {Button, StyleSheet, View} from 'react-native'; import {S3Client, PutObjectCommand} from '@aws-sdk/client-s3'; import {CognitoIdentityClient} from '@aws-sdk/client-cognito-identity'; import {fromCognitoIdentityPool} from '@aws-sdk/credential-provider-cognito-identity'; import {launchImageLibrary} from 'react-native-image-picker'; import RNFS from 'react-native-fs'; import {decode} from 'base64-arraybuffer'; const App = () => { const region = 'ap-northeast-1'; const bucket = 'sample-bucket'; const poolId = 'ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; const client = new S3Client({ region, credentials: fromCognitoIdentityPool({ client: new CognitoIdentityClient({region}), identityPoolId: poolId, }), }); const chooseMovie = () => { let options = { mediaType: 'video', }; launchImageLibrary(options, response => { if (response.didCancel) { console.log('Cancelled'); } else if (response.error) { console.log('Error: ', response.error); } else { const file = { uri: response.assets[0].uri, name: response.assets[0].fileName, type: 'video/quicktime', size: response.assets[0].fileSize, }; uploadVideo(file); } }); }; const uploadVideo = async file => { let contentType = 'video/quicktime'; let contentDeposition = 'inline;filename="' + file.name + '"'; const fileData = await RNFS.readFile(file.uri, 'base64'); const buffer = decode(fileData); const params = { Bucket: bucket, Key: file.name, Body: buffer, ContentDisposition: contentDeposition, ContentType: contentType, }; try { const response = await client.send(new PutObjectCommand(params)); console.log('success'); } catch (error) { console.log({error}); } }; return ( <View style={styles.container}> <View> <Button backroundColor="#68a0cf" title="Choose file" onPress={chooseMovie} /> </View> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, }); export default App;