続：ReactNative（Expo）でカメラで撮った写真をFiresbase Storageに保存する


[前にも記事を書いた](https://qiita.com/zaburo/items/0e014401e55c53395ed2)のですが、いろいろなところからの寄せ集めだったので改めてまとめてみました。

## 主な流れ

Expo + Firebaseを利用したアップロード処理は以下のようになります。

* CAMERAパーミッションを取得
* ImagePckerを起動し写真を取る（ファイルを選ぶ）
* アップできるよう取得したファイルをblobに変換
* アップロード先を指定してアップロード
* プログレス等を処理
* downloadURLを取得して表示に利用

## サンプルの仕様

ボタンとか配置するのがめんどくさいのでreac-native-elementsのAvatarを利用して画像の選択、表示をやってみます。
まあProfile画像とかでも使いますしね。Imageとかでも同じです（コメントアウトして入れてます）。

![cap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/13c43a3d-37e6-284b-3449-b3519bfbd305.png "cap.png")

いちおうプログレスとかも拾ってみます。

## 実装

### 必要モジュールのインストール

シンプルなわりにいろいろ使ってます。

```bash
npm install --save expo-constants expo-permissions expo-image-picker
npm install --save firebase react-native-elements
```

### Firebase

Firebaseへの登録やプロジェクト作成は当然として、App.jsと同じ階層にFirebase.jsを設置し、以下のようにしています。
Config情報は各自の環境に合わせます。

```js:Firebase.js
import firebase from 'firebase';

const firebaseConfig = {
    apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    authDomain: "xxxxxxxx.firebaseapp.com",
    databaseURL: "https://xxxxxxxx.firebaseio.com",
    projectId: "xxxxxxxxx",
    storageBucket: "xxxxxxxxx.appspot.com",
    messagingSenderId: "xxxxxxxxxx",
    appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    measurementId: "xxxxxxxxx"
};

firebase.initializeApp(firebaseConfig);
export default firebase;
```

### 実装

流れがわかるように１つの関数に実装しています。

>以前実装した際はPermissionはCAMERA_ROLLを取得する必要がありましたが、今回はCAMERAじゃないとエラーになりました。

```js:App.js
import React from 'react';
import { StyleSheet, Text, View, Image } from 'react-native';

import { Avatar, Button } from 'react-native-elements';
import firebase from './Firebase';
import Constants from 'expo-constants';
import * as ImagePicker from 'expo-image-picker';
import * as Permissions from 'expo-permissions';

class App extends React.Component {

    state = {
        url: 'https://firebasestorage.googleapis.com/v0/b/test-fc01e.appspot.com/o/noimage.png?alt=media&token=0606e1b4-e817-4859-83d4-7920c99e14c0',
        progress: '',
    }

    ImageChoiceAndUpload = async () => {

        try {

            //まず、CAMERA_ROLLのパーミッション確認
            if (Constants.platform.ios) {
                const { status } = await Permissions.askAsync(Permissions.CAMERA);
                if (status !== 'granted') {
                    alert("利用には許可が必要です。");
                    return;
                }
            }

            //次に、画像を選ぶ
            const result = await ImagePicker.launchCameraAsync();
            if (!result.cancelled) {

                //撮影された（ローカルの）写真を取得
                const localUri = await fetch(result.uri);
                //blobを取得
                const localBlob = await localUri.blob();

                //filename 実際はUIDとかユーザー固有のIDをファイル名にする感じかと
                const filename = "profileImage"

                //firebase storeのrefを取得
                const storageRef = firebase.storage().ref().child("images/" + filename);

                //upload
                // const putTask = await storageRef.put(localBlob);
                //進捗を取得したいのでawaitは使わず
                const putTask = storageRef.put(localBlob);
                putTask.on('state_changed', (snapshot) => {
                    let progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
                    this.setState({
                        progress: parseInt(progress) + "%",
                    });
                }, (error) => {
                    console.log(error);
                    alert("アップロードに失敗しました。サイズが大きいとか。");
                }, () => {
                    putTask.snapshot.ref.getDownloadURL().then(downloadURL => {
                        console.log(downloadURL);
                        this.setState({
                            progress: '',
                            url: downloadURL,
                        });
                    })
                })
            }
        } catch (e) {
            console.log(e.message);
            alert("サイズが多き過ぎるかも。");
        }

    }

    render() {
        return (
            <View style={{ marginTop: 100, alignSelf: 'center' }}>
                <Text>画像アップロード</Text>
                <View style={{ margin: 20 }}>
                    <Avatar
                        size="large"
                        rounded
                        title="NI"
                        onPress={this.ImageChoiceAndUpload}
                        source={{ uri: this.state.url }}
                    />
                    <Text style={{ alignSelf: 'center' }}>{this.state.progress}</Text>
                </View>
                {/* <Image
                    source={{ uri: this.state.url }}
                    style={{ width: 100, height: 100, alignSelf: 'center' }}
                /> */}
            </View>
        );
    }
}

export default App;
```

## その他

アップロードの容量制限等をしたいところですが、Storageのルールを使うこともできるようです。あとはextensionのresizeとかもいいかもしれません。

```
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read;
      allow write: if request.resource.size < 3 * 1024 * 1024; //3Mまで
    }
  }
}
```