0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ImagePickerのfile URIをDBに保存しない:React Nativeで画像を永続化する設計

0
Last updated at Posted at 2026-10-06

Expo ImagePickerで選んだ画像をそのままDBへ保存すると、あとから画像が読めなくなることがあります。

理由は、ImagePickerが返すuriが端末内のローカルファイルを指すことがあるためです。Expo公式ドキュメントの例でも、file:///.../cache/... のようなURIが返っています。

端末内URIは「その端末で今読める場所」であって、複数端末や再インストール後まで使える共有URLではありません。

まず結論

共有データとして画像を扱うなら、保存境界を次のようにします。

ImagePicker
  ↓
端末内のlocal URI
  ↓
Cloud Storageへアップロード
  ↓
Storage上の参照/URL
  ↓
DBへ保存

DBへ書く前に、画像を永続ストレージへ移すのがポイントです。

local URIをそのまま保存すると何が起きるか

const result = await ImagePicker.launchImageLibraryAsync();

if (!result.canceled) {
  const localUri = result.assets[0].uri;
}

このlocalUriをFirestoreなどへそのまま保存すると、別端末からは読めません。

さらにURIがキャッシュやアプリ管理下の一時領域を指す場合、そのファイルを共有データの永続参照として扱うのは危険です。

そのため、ローカルURIは「アップロード前の一時入力」として扱います。

永続化の責務をrepositoryに寄せる

作成画面と編集画面でアップロード処理を個別実装すると挙動がずれやすいため、保存境界へ寄せます。

type PhotoInput =
  | { type: 'keep' }
  | { type: 'remove' }
  | { type: 'replace'; localUri: string };
  • keep: 既存画像を変更しない
  • remove: 画像を削除する
  • replace: 新画像をアップロードして差し替える

string | null | undefinedだけで扱うより、意図を型で表せます。

アップロード後にDBを書き込む

async function saveItem(input: PhotoInput) {
  let uploadedPath: string | null = null;

  try {
    const photoUrl =
      input.type === 'replace'
        ? await uploadPhoto(input.localUri, path => {
            uploadedPath = path;
          })
        : input.type === 'remove'
          ? null
          : undefined;

    await saveToDatabase({ photoUrl });
  } catch (error) {
    if (uploadedPath) {
      await deleteUploadedFile(uploadedPath).catch(() => {
        // cleanup失敗は別ログへ
      });
    }

    throw error;
  }
}

アップロード失敗時にDBだけ成功した状態を作らないことが重要です。

逆に、アップロード後のDB保存で失敗した場合は、その保存試行で新規作成したファイルだけを削除します。

これは補償処理という考え方で、複数システムをまたぐ途中失敗を打ち消します。

Storage Rulesでも検証する

Firebase Storage Security Rulesでは、認証だけでなくファイルサイズやMIMEタイプも検証できます。

allow write: if request.auth != null
  && request.resource.size < 10 * 1024 * 1024
  && request.resource.contentType.matches('image/.*');

10MiBはFirebaseの上限ではなく、アプリ側で決めた例です。

クライアントの事前チェックだけに頼らず、Storage Rulesでも制限します。

上書きより新規パスを使う

差し替え時に同じStorageパスを上書きすると、DB更新失敗時に以前の画像まで失う可能性があります。

新画像はUUIDなどを含む別パスへ保存し、

  1. 新画像をアップロード
  2. DBの参照先を更新
  3. 必要なら旧画像を削除

という順序にすると途中失敗に強くなります。

テストはデータの寿命をまたぐ

最低限、次を確認します。

  • DBにはlocal URIではなく永続参照が入る
  • 元のローカル画像を消しても表示できる
  • アプリ再起動後も表示できる
  • 別画面や共有画像生成からも読める
  • Rulesで未認証・権限なし・不正MIME・過大ファイルを拒否できる
  • DB保存失敗時に今回アップロードしたファイルだけcleanupされる

選択直後に同じ画面で見えるだけでは不十分です。

まとめ

  • local URIは一時入力として扱う
  • DB書き込み前にCloud Storageへ移す
  • keep/remove/replaceを型で分ける
  • StorageとDBの途中失敗には補償処理を入れる
  • Security Rulesでもサイズ・MIME・権限を検証する
  • 再起動や元ファイル削除まで含めてテストする

この設計は、自分が開発している旅行サポートアプリ TripSupporter の予定写真保存でも使っています。

この修正は 2026年10月4日時点では未リリース です。コードのmain反映と、App Storeでユーザーへ配布される状態は分けて管理しています。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?