ponshu-room-lite/lib/services/migration_service.dart

130 lines
4.8 KiB
Dart

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<void> runMigration() async {
debugPrint('[Migration] Starting Phase 0 Migration...');
// 1. Open Boxes
final box = await Hive.openBox<SakeItem>(_boxName);
// 2. Backup Strategy
try {
final backupBox = await Hive.openBox<SakeItem>(_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<void> compressAllExistingImages() async {
debugPrint('[Migration] Starting Image Compression...');
final box = Hive.box<SakeItem>(_boxName);
int compressedCount = 0;
for (var key in box.keys) {
final SakeItem? item = box.get(key);
if (item == null) continue;
bool changed = false;
List<String> 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.');
}
}