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('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.from(item.displayData.imagePaths); final newPaths = []; 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('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); } } }