ponshu-room-lite/CRITICAL_BUG_IMAGE_DELETION.md

249 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🚨 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