249 lines
8.4 KiB
Markdown
249 lines
8.4 KiB
Markdown
|
|
# 🚨 Critical Bug Report: 画像誤削除問題
|
|||
|
|
|
|||
|
|
**発見日**: 2026-01-22
|
|||
|
|
**影響**: ⚠️ ユーザーデータ消失
|
|||
|
|
**ステータス**: ✅ 修正完了
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 問題の概要
|
|||
|
|
|
|||
|
|
### 症状
|
|||
|
|
- カード一覧・詳細画面で日本酒の写真が表示されない
|
|||
|
|
- ストレージ: 558MB → 409MB(Androidキャッシュクリア後)
|
|||
|
|
- 一部の画像ファイルが消失
|
|||
|
|
|
|||
|
|
### 影響範囲
|
|||
|
|
- **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
|