8.4 KiB
8.4 KiB
🚨 Critical Bug Report: 画像誤削除問題
発見日: 2026-01-22
影響: ⚠️ ユーザーデータ消失
ステータス: ✅ 修正完了
📊 問題の概要
症状
- カード一覧・詳細画面で日本酒の写真が表示されない
- ストレージ: 558MB → 409MB(Androidキャッシュクリア後)
- 一部の画像ファイルが消失
影響範囲
- 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
🚨 ユーザーへの影響と対応
影響を受けたユーザー
- 一時ファイルクリーンアップを実行したユーザー
- 症状: 一部の日本酒写真が表示されない
復旧方法
❌ 復旧不可能
- 削除された画像ファイルは復元できません
- バックアップがない場合、データ損失
対応策
- ユーザーに謝罪
- 削除された日本酒を再撮影してもらう
- 今後はバックアップ機能を強化
📋 修正ファイル一覧
-
lib/services/image_batch_compression_service.dartcleanupTempFiles()を修正- スキャン対象を
getTemporaryDirectory()に変更
-
lib/screens/camera_screen.dart- ギャラリー用一時ファイルを
getTemporaryDirectory()に保存
- ギャラリー用一時ファイルを
-
lib/services/image_compression_service.dart_generateCompressedPath()を修正- 一時ファイルを
getTemporaryDirectory()に保存
✅ テスト計画
1. 一時ファイルクリーンアップのテスト
- 日本酒を3枚撮影
- 開発者メニュー → 一時ファイルクリーンアップ
- 確認: 日本酒の写真がすべて表示されることを確認
- 確認: ストレージ使用量が増加していないことを確認
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. 一括圧縮のテスト
- 既存画像を一括圧縮
- 確認: すべての日本酒の写真が表示されることを確認
- 確認: ストレージ使用量が削減されたことを確認
📝 教訓
❌ やってはいけないこと
- 永続ファイルと一時ファイルを同じディレクトリに保存
- ファイル名の接尾辞でファイルタイプを判定
contains()で部分一致検索して削除
✅ やるべきこと
- 永続ファイルと一時ファイルを別のディレクトリに保存
- ディレクトリ構成で責任を分離
- 削除前に慎重に確認
🎯 今後の改善
- バックアップ機能の強化(自動バックアップ)
- 削除前の確認ダイアログ(ファイル一覧表示)
- ユニットテスト追加(ファイル操作)
作成日: 2026-01-22
作成者: Cursor AI
レビュアー: 必要
優先度: 🔴 Critical