import 'dart:io'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import '../models/sake_item.dart'; /// 画像パス修復サービス /// /// バックアップ復元後に画像パスが不整合になった場合の修復 class ImagePathRepairService { /// 画像パスの整合性をチェック /// /// 戻り値: (総アイテム数, 問題のあるアイテム数, 欠損ファイル数) static Future<(int, int, int)> diagnose() async { try { final box = Hive.box('sake_items'); final items = box.values.toList(); int totalItems = items.length; int problematicItems = 0; int missingFiles = 0; debugPrint('🔍 画像パス診断開始: $totalItems アイテム'); for (final item in items) { bool hasIssue = false; for (final imagePath in item.displayData.imagePaths) { final file = File(imagePath); if (!await file.exists()) { debugPrint('❌ Missing: $imagePath (${item.displayData.displayName})'); missingFiles++; hasIssue = true; } } if (hasIssue) { problematicItems++; } } debugPrint('📊 診断結果: $totalItems アイテム中 $problematicItems に問題あり ($missingFiles ファイル欠損)'); return (totalItems, problematicItems, missingFiles); } catch (e) { debugPrint('❌ 診断エラー: $e'); return (0, 0, 0); } } /// 画像パスを修復 /// /// 戦略: /// 1. 存在しないパスを検出 /// 2. getApplicationDocumentsDirectory() 内の実際のファイルを探す /// 3. ファイル名(UUID)で照合 /// 4. パスを更新 /// /// 戻り値: (修復したアイテム数, 修復した画像パス数) static Future<(int, int)> repair() async { try { final box = Hive.box('sake_items'); final items = box.values.toList(); final appDir = await getApplicationDocumentsDirectory(); // アプリディレクトリ内のすべての画像ファイルを取得 final availableFiles = []; final dir = Directory(appDir.path); await for (final entity in dir.list()) { if (entity is File) { final fileName = path.basename(entity.path); if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png')) { availableFiles.add(entity.path); } } } debugPrint('📁 利用可能な画像ファイル: ${availableFiles.length}個'); int repairedItems = 0; int repairedPaths = 0; for (final item in items) { bool needsRepair = false; List newPaths = []; for (final oldPath in item.displayData.imagePaths) { final file = File(oldPath); if (await file.exists()) { // パスが有効な場合はそのまま newPaths.add(oldPath); } else { // パスが無効な場合、ファイル名で照合 final oldFileName = path.basename(oldPath); // 完全一致を探す String? matchedPath; for (final availablePath in availableFiles) { if (path.basename(availablePath) == oldFileName) { matchedPath = availablePath; break; } } if (matchedPath != null) { newPaths.add(matchedPath); repairedPaths++; needsRepair = true; debugPrint('🔧 Repaired: $oldFileName -> $matchedPath'); } else { // マッチしない場合、警告してスキップ debugPrint('⚠️ No match for: $oldFileName (${item.displayData.displayName})'); } } } if (needsRepair && newPaths.isNotEmpty) { // パスを更新 final updatedItem = item.copyWith( imagePaths: newPaths, ); await box.put(item.key, updatedItem); // 🔧 Fixed: updatedItem を保存 repairedItems++; debugPrint('✅ Updated: ${item.displayData.displayName} (${newPaths.length} paths)'); } } debugPrint('✅ 修復完了: $repairedItems アイテム、$repairedPaths パス'); return (repairedItems, repairedPaths); } catch (e) { debugPrint('❌ 修復エラー: $e'); return (0, 0); } } /// 孤立したファイルを検出(Hiveに参照されていない画像ファイル) /// /// 戻り値: (孤立ファイル数, 合計サイズ(bytes)) static Future<(int, int)> findOrphanedFiles() async { try { final box = Hive.box('sake_items'); final items = box.values.toList(); final appDir = await getApplicationDocumentsDirectory(); // Hiveに登録されているすべての画像パスを収集 final registeredPaths = {}; for (final item in items) { registeredPaths.addAll(item.displayData.imagePaths); } // アプリディレクトリ内のすべての画像ファイルを取得 int orphanedCount = 0; int totalSize = 0; final dir = Directory(appDir.path); await for (final entity in dir.list()) { if (entity is File) { final fileName = path.basename(entity.path); if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png')) { if (!registeredPaths.contains(entity.path)) { final size = await entity.length(); orphanedCount++; totalSize += size; debugPrint('🗑️ Orphaned: $fileName (${(size / 1024).toStringAsFixed(1)}KB)'); } } } } debugPrint('📊 孤立ファイル: $orphanedCount 個 (${(totalSize / 1024 / 1024).toStringAsFixed(1)}MB)'); return (orphanedCount, totalSize); } catch (e) { debugPrint('❌ 孤立ファイル検出エラー: $e'); return (0, 0); } } /// 孤立ファイルを削除 /// /// 戻り値: (削除したファイル数, 削減したサイズ(bytes)) static Future<(int, int)> cleanOrphanedFiles() async { try { final box = Hive.box('sake_items'); final items = box.values.toList(); final appDir = await getApplicationDocumentsDirectory(); // Hiveに登録されているすべての画像パスを収集 final registeredPaths = {}; for (final item in items) { registeredPaths.addAll(item.displayData.imagePaths); } // アプリディレクトリ内のすべての画像ファイルを取得 int deletedCount = 0; int deletedSize = 0; final dir = Directory(appDir.path); await for (final entity in dir.list()) { if (entity is File) { final fileName = path.basename(entity.path); if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png')) { if (!registeredPaths.contains(entity.path)) { final size = await entity.length(); await entity.delete(); deletedCount++; deletedSize += size; debugPrint('🗑️ Deleted: $fileName (${(size / 1024).toStringAsFixed(1)}KB)'); } } } } debugPrint('✅ 孤立ファイル削除完了: $deletedCount 個 (${(deletedSize / 1024 / 1024).toStringAsFixed(1)}MB)'); return (deletedCount, deletedSize); } catch (e) { debugPrint('❌ 孤立ファイル削除エラー: $e'); return (0, 0); } } }