ExpoでActive StorageにDirect Upload
Reac Native勉強中。
APIサーバーでもRailsは大活躍しておりますが、ExpoでActive Storageを扱うにはちょっとコツが必要でした。
まずはImage Pickerはなんでも良いと思うのですが、複数画像を選択できるnpmが expo-image-picker-multiple
くらいだったのでこちらをインストール。
yarn add expo-image-picker-multiple
これを呼び出すわけですが、callbackで画像を扱いやすいサイズに縮小しつつ、その情報をbase64で取得します。
保存されたファイルをfetchして扱うとなぜかバイナリが変更されてしまうので、このbase64を取り回す必要がありました。
ついでにファイルサイズとmd5も取得します。
import { ImageBrowser } from 'expo-image-picker-multiple'; import * as ImageManipulator from 'expo-image-manipulator'; import * as FileSystem from 'expo-file-system'; async function processImageAsync(uri) { const file = await ImageManipulator.manipulateAsync( uri, [{ resize: { width: 1000 } }], { compress: 0.8, format: ImageManipulator.SaveFormat.JPEG, base64: true }, ); return file; } <ImageBrowser max={20} loadCount={50} onChange={(num, onSubmit) => { setNum(num); setOnSubmit({ fn: onSubmit, }); }} callback={(callback) => { setLoading(true); callback .then(async (photos) => { const cPhotos = []; for (const photo of photos) { const pPhoto = await processImageAsync(photo.uri); const info = await FileSystem.getInfoAsync(pPhoto.uri, { md5: true, size: true, }); cPhotos.push({ url: pPhoto.uri, filename: photo.filename, width: pPhoto.width, height: pPhoto.height, type: 'image/jpg', size: info.size, checksum: info.md5, base64: pPhoto.base64, }); } setValue('imageFiles', cPhotos); navigation.goBack(); }) .catch((e) => { console.log(e); setLoading(false); }); }} />
今回recordを作成するのですがAPIは下記のように実際にrecordを作成するのとActiveStorage用のAPIは2つを利用します。
blob: async (auth: Auth, file: File): Promise<BlobData> => { const client = new ApiClient<BlobData>(auth); return await client.create('/rails/active_storage/direct_uploads', { blob: { filename: file.filename, contentType: file.type, byteSize: file.size, checksum: base64.fromByteArray(hexStringToByte(file.checksum)), }, }); }, directUpload: async (file: File, blob: BlobData): Promise<void> => { const fileBlob = base64.toByteArray(file.base64); const options = { headers: { 'Content-Type': blob.directUpload.headers.ContentType, 'Content-MD5': blob.directUpload.headers.ContentMD5, }, }; await axios.put(blob.directUpload.url, fileBlob, options); }, create: async (auth: Auth, form: RecordForm): Promise<Record> => { const client = new ApiClient<Record>(auth); console.log({ record: form }); return await client.create('/api/records', { record: form }); },
これを下記のように
- 画像ごとにactive storageのblobを作成してその情報でdirectUploadする
- 画像情報を加えてアップロード
という感じでやればOKです。
const images = await Promise.all( imageFiles.map(async (imageFile) => { const blob = await RecordApi.blob(state.auth, imageFile); await RecordApi.directUpload(imageFile, blob); return blob.signedId; }), ); await RecordApi.create(state.auth, { ...form, images, });
だいぶ適当でしたが参考までに。
#アップロード中の途中経過を出すのはいったんパスしました。