import 'package:flutter/foundation.dart'; // debugPrint import 'package:hive_flutter/hive_flutter.dart'; import '../models/sake_item.dart'; import 'image_compression_service.dart'; class MigrationService { static const String _boxName = 'sake_items'; static const String _backupBoxName = 'sake_items_backup'; /// Runs the migration process with safety backup. /// Should be called after Hive.init and Adapter registration, but before app UI loads. static Future runMigration() async { debugPrint('[Migration] Starting Phase 0 Migration...'); // 1. Open Boxes final box = await Hive.openBox(_boxName); // 2. Backup Strategy try { final backupBox = await Hive.openBox(_backupBoxName); if (backupBox.isEmpty && box.isNotEmpty) { debugPrint('[Migration] detailed backup started...'); // Copy all for (var key in box.keys) { final SakeItem? item = box.get(key); if (item != null) { await backupBox.put(key, item.copyWith()); } } debugPrint('[Migration] Backup completed. ${backupBox.length} items secured.'); } else { debugPrint('[Migration] Backup skipped (Existing backup found or Empty source).'); } // Close backup to ensure flush await backupBox.close(); } catch (e) { debugPrint('[Migration] CRITICAL ERROR during Backup: $e'); // If backup fails, do we abort? // Yes, abort migration to be safe. return; } // 3. Migration (In-Place) int migratedCount = 0; for (var key in box.keys) { final SakeItem? item = box.get(key); if (item != null) { try { // ensureMigrated checks if displayData is null. // If null, it populates it from legacy fields. bool performed = item.ensureMigrated(); if (performed) { await item.save(); // Persist the new structure (DisplayData, etc) migratedCount++; } } catch (e) { debugPrint('[Migration] Error migrating item $key: $e'); } } } if (migratedCount > 0) { debugPrint('[Migration] Successfully migrated $migratedCount items to Schema v2.0.'); } else { debugPrint('[Migration] No items needed migration.'); } } /// CR-005: 既存画像の圧縮マイグレーション /// 500MB超の既存画像を圧縮してストレージを解放する static Future compressAllExistingImages() async { debugPrint('[Migration] Starting Image Compression...'); final box = Hive.box(_boxName); int compressedCount = 0; for (var key in box.keys) { final SakeItem? item = box.get(key); if (item == null) continue; bool changed = false; List newPaths = []; for (String path in item.displayData.imagePaths) { // 既に圧縮済みっぽいファイル名ならスキップ (簡易チェック) if (path.contains('_compressed')) { newPaths.add(path); continue; } try { // 圧縮実行 (1024px, 85%) // 元ファイルを置き換えるのではなく、新しいパスを取得 final String newPath = await ImageCompressionService.compressForGemini(path); if (newPath != path) { newPaths.add(newPath); changed = true; // 元画像は? 安全のため一旦保持するか、削除するか。 // 容量削減が目的なので、成功したら元画像は削除したいが、 // ユーザーのファイルを勝手に消すのはリスクがあるため、今回は「新しいパスへの切り替え」のみ行う。 // (OSの一時ファイル削除に任せる、または後でクリーンアップ) // が、Storage Rescueの文脈では「容量削減」なので削除すべきだが、 // ImageCompressionServiceの実装では '_compressed' を別名で作る。 // 元ファイルがユーザーのギャラリーにある場合、それを消すとオリジナルが消える。 // アプリ内コピーであれば消して良い。 // 安全策: パスだけ更新。圧縮ファイルを使うようにする。 } else { newPaths.add(path); } } catch (e) { debugPrint('[Migration] Error compressing image for ${item.displayData.displayName}: $e'); newPaths.add(path); // エラー時は元パス維持 } } if (changed) { final newItem = item.copyWith( imagePaths: newPaths, ); await box.put(key, newItem); compressedCount++; } } debugPrint('[Migration] Image compression completed. Updated $compressedCount items.'); } }