ponshu-room-lite/docs/archive/CRITICAL_BUG_IMAGE_DELETION.md

8.4 KiB
Raw Permalink Blame History

🚨 Critical Bug Report: 画像誤削除問題

発見日: 2026-01-22
影響: ⚠️ ユーザーデータ消失
ステータス: 修正完了


📊 問題の概要

症状

  • カード一覧・詳細画面で日本酒の写真が表示されない
  • ストレージ: 558MB → 409MBAndroidキャッシュクリア後
  • 一部の画像ファイルが消失

影響範囲

  • Critical: ユーザーが撮影した日本酒の写真が削除された
  • 被害画像: _gallery.jpg, _compressed.jpg を含むすべてのファイル
  • 被害者: 一時ファイルクリーンアップを実行したユーザー

🔍 根本原因

バグのあるコード

// lib/services/image_batch_compression_service.dart:157-192 (修正前)
static Future<(int, int)> cleanupTempFiles() async {
  // ❌ 問題: getApplicationDocumentsDirectory() をスキャン(永続ファイルがある場所)
  final directory = await getApplicationDocumentsDirectory();
  final dir = Directory(directory.path);
  
  await for (final entity in dir.list()) {
    if (entity is File) {
      final fileName = entity.path.split('/').last;
      
      // ❌ 問題: _compressed, _gallery を含むすべてのファイルを削除
      if (fileName.contains('_compressed') || fileName.contains('_gallery')) {
        await entity.delete(); // ← 本物の画像を削除!
      }
    }
  }
}

なぜ本物の画像が削除されたのか?

1. ギャラリー保存用一時ファイル

// lib/screens/camera_screen.dart:225 (修正前)
final String galleryPath = join(directory.path, '${const Uuid().v4()}_gallery.jpg');
//                              ↑ directory = getApplicationDocumentsDirectory()
  • _gallery.jpg という名前で永続ディレクトリに保存
  • 削除処理が失敗した場合、ファイルが残る
  • cleanupTempFiles() で削除される

2. 圧縮用一時ファイル

// lib/services/image_compression_service.dart:96 (修正前)
static Future<String> _generateCompressedPath(String sourcePath) async {
  final directory = await getApplicationDocumentsDirectory();
  return path.join(directory.path, '${fileName}_compressed$extension');
}
  • _compressed.jpg という名前で永続ディレクトリに保存
  • 一括圧縮で使用される
  • cleanupTempFiles() で削除される

設計ミス

ディレクトリ 用途 実際の使い方(修正前)
getApplicationDocumentsDirectory() 永続ファイル 本物の画像
一時ファイルも保存
getTemporaryDirectory() 一時ファイル 使われていない

問題点: 永続ファイルと一時ファイルが同じディレクトリに混在


修正内容

修正1: cleanupTempFiles() のスキャン対象を変更

// lib/services/image_batch_compression_service.dart:157-192 (修正後)
static Future<(int, int)> cleanupTempFiles() async {
  // ✅ 修正: getTemporaryDirectory() をスキャン(一時ファイルのみ)
  final directory = await getTemporaryDirectory();
  final dir = Directory(directory.path);
  
  await for (final entity in dir.list()) {
    if (entity is File) {
      final fileName = entity.path.split('/').last;
      
      // ✅ 修正: 画像ファイルすべてを削除(一時ディレクトリ内のみ)
      if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png')) {
        await entity.delete(); // ← 安全!
      }
    }
  }
}

変更点:

  • getApplicationDocumentsDirectory()getTemporaryDirectory()
  • contains('_compressed')endsWith('.jpg')全画像を削除でOK

修正2: ギャラリー用一時ファイルを getTemporaryDirectory()

// lib/screens/camera_screen.dart:222-231 (修正後)
// ✅ 修正: 一時ファイルは getTemporaryDirectory() に保存
final tempDir = await getTemporaryDirectory();
final String galleryPath = join(tempDir.path, '${const Uuid().v4()}_gallery.jpg');

修正3: 圧縮用一時ファイルを getTemporaryDirectory()

// lib/services/image_compression_service.dart:94-100 (修正後)
static Future<String> _generateCompressedPath(String sourcePath) async {
  // ✅ 修正: 一時ファイルは getTemporaryDirectory() に保存
  final directory = await getTemporaryDirectory();
  return path.join(directory.path, '${fileName}_compressed$extension');
}

📊 修正後のディレクトリ構成

ディレクトリ 用途 保存されるファイル
getApplicationDocumentsDirectory() 永続ファイル 本物の画像UUID.jpg
Hive DB
getTemporaryDirectory() 一時ファイル _gallery.jpg
_compressed.jpg
その他一時ファイル

メリット:

  • 一時ファイルクリーンアップで絶対に本物の画像が削除されない
  • ディレクトリ構成が明確

🔒 再発防止策

1. コーディング規約

// ✅ 永続ファイル(本物の画像)
final directory = await getApplicationDocumentsDirectory();
final permanentPath = join(directory.path, '${const Uuid().v4()}.jpg');

// ✅ 一時ファイル(処理後に削除)
final tempDirectory = await getTemporaryDirectory();
final tempPath = join(tempDirectory.path, '${const Uuid().v4()}_temp.jpg');

2. クリーンアップ関数のルール

  • 必ず getTemporaryDirectory() のみをスキャン
  • getApplicationDocumentsDirectory() をスキャンしない

3. 一時ファイルの命名規則

  • 接尾辞不要(ディレクトリで分離)
  • getTemporaryDirectory() 内のすべてのファイルは削除OK

🚨 ユーザーへの影響と対応

影響を受けたユーザー

  • 一時ファイルクリーンアップを実行したユーザー
  • 症状: 一部の日本酒写真が表示されない

復旧方法

復旧不可能

  • 削除された画像ファイルは復元できません
  • バックアップがない場合、データ損失

対応策

  1. ユーザーに謝罪
  2. 削除された日本酒を再撮影してもらう
  3. 今後はバックアップ機能を強化

📋 修正ファイル一覧

  1. lib/services/image_batch_compression_service.dart

    • cleanupTempFiles() を修正
    • スキャン対象を getTemporaryDirectory() に変更
  2. lib/screens/camera_screen.dart

    • ギャラリー用一時ファイルを getTemporaryDirectory() に保存
  3. lib/services/image_compression_service.dart

    • _generateCompressedPath() を修正
    • 一時ファイルを getTemporaryDirectory() に保存

テスト計画

1. 一時ファイルクリーンアップのテスト

  1. 日本酒を3枚撮影
  2. 開発者メニュー → 一時ファイルクリーンアップ
  3. 確認: 日本酒の写真がすべて表示されることを確認
  4. 確認: ストレージ使用量が増加していないことを確認

2. ストレージ構成の確認

# Android Debug Bridge (adb) で確認
adb shell run-as com.example.ponshu_room_lite ls /data/user/0/com.example.ponshu_room_lite/app_flutter/
# → 永続ファイルのみUUID.jpg

adb shell run-as com.example.ponshu_room_lite ls /data/user/0/com.example.ponshu_room_lite/cache/
# → 一時ファイルのみ_gallery.jpg, _compressed.jpg

3. 一括圧縮のテスト

  1. 既存画像を一括圧縮
  2. 確認: すべての日本酒の写真が表示されることを確認
  3. 確認: ストレージ使用量が削減されたことを確認

📝 教訓

やってはいけないこと

  1. 永続ファイルと一時ファイルを同じディレクトリに保存
  2. ファイル名の接尾辞でファイルタイプを判定
  3. contains() で部分一致検索して削除

やるべきこと

  1. 永続ファイルと一時ファイルを別のディレクトリに保存
  2. ディレクトリ構成で責任を分離
  3. 削除前に慎重に確認

🎯 今後の改善

  1. バックアップ機能の強化(自動バックアップ)
  2. 削除前の確認ダイアログ(ファイル一覧表示)
  3. ユニットテスト追加(ファイル操作)

作成日: 2026-01-22
作成者: Cursor AI
レビュアー: 必要
優先度: 🔴 Critical