ponshu-room-lite/lib/services/image_batch_compression_ser...

202 lines
7.3 KiB
Dart
Raw Permalink Normal View History

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);
}
}
}