202 lines
7.3 KiB
Dart
202 lines
7.3 KiB
Dart
import 'dart:io';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:hive_flutter/hive_flutter.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import '../models/sake_item.dart';
|
||
import 'image_compression_service.dart';
|
||
|
||
// ⚠️ Critical Fix (Day 5.5): cleanupTempFiles() を修正
|
||
// 問題: getApplicationDocumentsDirectory() をスキャンして _compressed, _gallery を削除
|
||
// 結果: 本物の画像を誤削除
|
||
// 修正: getTemporaryDirectory() のみをスキャン
|
||
|
||
/// 既存画像の一括圧縮サービス
|
||
///
|
||
/// 用途: アプリ更新後、既存の未圧縮画像を圧縮してストレージを削減
|
||
class ImageBatchCompressionService {
|
||
/// 既存の画像を一括圧縮
|
||
///
|
||
/// 処理内容:
|
||
/// 1. すべての SakeItem から画像パスを取得
|
||
/// 2. 各画像を圧縮(1024px, JPEG 85%)
|
||
/// 3. 元画像を削除
|
||
/// 4. SakeItem の imagePaths を更新
|
||
///
|
||
/// 戻り値: (圧縮成功数, 圧縮失敗数, 削減したバイト数)
|
||
static Future<(int, int, int)> compressAllImages({
|
||
required Function(int current, int total, String fileName) onProgress,
|
||
}) async {
|
||
final box = Hive.box<SakeItem>('sake_items');
|
||
final allItems = box.values.toList();
|
||
|
||
int successCount = 0;
|
||
int failedCount = 0;
|
||
int savedBytes = 0;
|
||
int totalImages = 0;
|
||
|
||
// 全画像数をカウント
|
||
for (final item in allItems) {
|
||
totalImages += item.displayData.imagePaths.length;
|
||
}
|
||
|
||
int processedCount = 0;
|
||
|
||
for (final item in allItems) {
|
||
final originalPaths = List<String>.from(item.displayData.imagePaths);
|
||
final newPaths = <String>[];
|
||
|
||
for (final originalPath in originalPaths) {
|
||
processedCount++;
|
||
final file = File(originalPath);
|
||
|
||
try {
|
||
// ファイルが存在するか確認
|
||
if (!await file.exists()) {
|
||
debugPrint('⚠️ File not found: $originalPath');
|
||
newPaths.add(originalPath); // パスをそのまま保持
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
// 元のファイルサイズを取得
|
||
final originalSize = await file.length();
|
||
|
||
// ファイル名から拡張子を取得
|
||
final fileName = originalPath.split('/').last;
|
||
onProgress(processedCount, totalImages, fileName);
|
||
|
||
// 既に圧縮済みか確認(ファイルサイズで判断)
|
||
if (originalSize < 500 * 1024) { // 500KB以下なら既に圧縮済み
|
||
debugPrint('✅ Already compressed: $fileName (${(originalSize / 1024).toStringAsFixed(1)}KB)');
|
||
newPaths.add(originalPath);
|
||
successCount++;
|
||
continue;
|
||
}
|
||
|
||
// Day 5: 安全な圧縮(一時ファイル経由)
|
||
debugPrint('🗜️ Compressing: $fileName (${(originalSize / 1024 / 1024).toStringAsFixed(1)}MB)');
|
||
|
||
// 1. 一時ファイルに圧縮(targetPathを指定しない)
|
||
final tempCompressedPath = await ImageCompressionService.compressForGemini(originalPath);
|
||
|
||
// 2. 圧縮後のサイズを取得
|
||
final compressedSize = await File(tempCompressedPath).length();
|
||
final saved = originalSize - compressedSize;
|
||
savedBytes += saved;
|
||
|
||
debugPrint('✅ Compressed: $fileName - ${(originalSize / 1024).toStringAsFixed(1)}KB → ${(compressedSize / 1024).toStringAsFixed(1)}KB (${(saved / 1024).toStringAsFixed(1)}KB saved)');
|
||
|
||
// 3. 圧縮成功後に元ファイルを削除
|
||
try {
|
||
await file.delete();
|
||
debugPrint('🗑️ Deleted original: $originalPath');
|
||
} catch (e) {
|
||
debugPrint('⚠️ Failed to delete original: $e');
|
||
// エラー時は一時ファイルを削除して元のパスを保持
|
||
await File(tempCompressedPath).delete();
|
||
newPaths.add(originalPath);
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
// 4. 一時ファイルを元の場所に移動
|
||
try {
|
||
await File(tempCompressedPath).rename(originalPath);
|
||
debugPrint('📦 Moved compressed file to: $originalPath');
|
||
} catch (e) {
|
||
debugPrint('⚠️ Failed to rename file: $e');
|
||
// エラー時は一時ファイルをそのまま使用
|
||
newPaths.add(tempCompressedPath);
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
newPaths.add(originalPath);
|
||
successCount++;
|
||
|
||
} catch (e) {
|
||
debugPrint('❌ Failed to compress: $originalPath - $e');
|
||
newPaths.add(originalPath); // エラー時は元のパスを保持
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
// SakeItem の imagePaths を更新
|
||
if (newPaths.isNotEmpty) {
|
||
final updatedItem = item.copyWith(
|
||
imagePaths: newPaths,
|
||
);
|
||
await box.put(item.key, updatedItem);
|
||
}
|
||
}
|
||
|
||
return (successCount, failedCount, savedBytes);
|
||
}
|
||
|
||
/// ストレージ使用量を取得
|
||
///
|
||
/// 戻り値: (総ファイル数, 総バイト数)
|
||
static Future<(int, int)> getStorageUsage() async {
|
||
final box = Hive.box<SakeItem>('sake_items');
|
||
final allItems = box.values.toList();
|
||
|
||
int totalFiles = 0;
|
||
int totalBytes = 0;
|
||
|
||
for (final item in allItems) {
|
||
for (final path in item.displayData.imagePaths) {
|
||
final file = File(path);
|
||
if (await file.exists()) {
|
||
totalFiles++;
|
||
totalBytes += await file.length();
|
||
}
|
||
}
|
||
}
|
||
|
||
return (totalFiles, totalBytes);
|
||
}
|
||
|
||
/// 一時ファイルをクリーンアップ
|
||
///
|
||
/// 🔒 Safe: getTemporaryDirectory() のみをスキャン(永続ファイルは削除しない)
|
||
///
|
||
/// 戻り値: (削除したファイル数, 削減したバイト数)
|
||
static Future<(int, int)> cleanupTempFiles() async {
|
||
try {
|
||
// ⚠️ 重要: getTemporaryDirectory() を使用(getApplicationDocumentsDirectory() ではない)
|
||
final directory = await getTemporaryDirectory();
|
||
final dir = Directory(directory.path);
|
||
|
||
int deletedCount = 0;
|
||
int deletedBytes = 0;
|
||
|
||
// ディレクトリ内のすべてのファイルをスキャン
|
||
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')) {
|
||
try {
|
||
final fileSize = await entity.length();
|
||
await entity.delete();
|
||
deletedCount++;
|
||
deletedBytes += fileSize;
|
||
debugPrint('🗑️ Deleted temp file: $fileName (${(fileSize / 1024).toStringAsFixed(1)}KB)');
|
||
} catch (e) {
|
||
debugPrint('⚠️ Failed to delete temp file: $fileName - $e');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
debugPrint('✅ Cleanup complete: $deletedCount files, ${(deletedBytes / 1024 / 1024).toStringAsFixed(1)}MB');
|
||
|
||
return (deletedCount, deletedBytes);
|
||
} catch (e) {
|
||
debugPrint('❌ Cleanup error: $e');
|
||
return (0, 0);
|
||
}
|
||
}
|
||
}
|