ponshu-room-lite/CRITICAL_BUG_IMAGE_DELETION.md

249 lines
8.4 KiB
Markdown
Raw Normal View History

# 🚨 Critical Bug Report: 画像誤削除問題
**発見日**: 2026-01-22
**影響**: ⚠️ ユーザーデータ消失
**ステータス**: ✅ 修正完了
---
## 📊 問題の概要
### 症状
- カード一覧・詳細画面で日本酒の写真が表示されない
- ストレージ: 558MB → 409MBAndroidキャッシュクリア後
- 一部の画像ファイルが消失
### 影響範囲
- **Critical**: ユーザーが撮影した日本酒の写真が削除された
- **被害画像**: `_gallery.jpg`, `_compressed.jpg` を含むすべてのファイル
- **被害者**: 一時ファイルクリーンアップを実行したユーザー
---
## 🔍 根本原因
### バグのあるコード
```dart
// 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. ギャラリー保存用一時ファイル
```dart
// lib/screens/camera_screen.dart:225 (修正前)
final String galleryPath = join(directory.path, '${const Uuid().v4()}_gallery.jpg');
// ↑ directory = getApplicationDocumentsDirectory()
```
- `_gallery.jpg` という名前で永続ディレクトリに保存
- 削除処理が失敗した場合、ファイルが残る
- `cleanupTempFiles()` で削除される
#### 2. 圧縮用一時ファイル
```dart
// 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()` | **永続ファイル** | ✅ 本物の画像<br>❌ 一時ファイルも保存 |
| `getTemporaryDirectory()` | **一時ファイル** | ❌ 使われていない |
**問題点**: 永続ファイルと一時ファイルが同じディレクトリに混在
---
## ✅ 修正内容
### 修正1: `cleanupTempFiles()` のスキャン対象を変更
```dart
// 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()` へ
```dart
// 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()` へ
```dart
// 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<br>✅ Hive DB |
| `getTemporaryDirectory()` | **一時ファイル** | ✅ _gallery.jpg<br>✅ _compressed.jpg<br>✅ その他一時ファイル |
**メリット**:
- 一時ファイルクリーンアップで**絶対に本物の画像が削除されない**
- ディレクトリ構成が明確
---
## 🔒 再発防止策
### 1. コーディング規約
```dart
// ✅ 永続ファイル(本物の画像)
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. ストレージ構成の確認
```bash
# 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