130 lines
4.8 KiB
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.');
|
|
}
|
|
}
|