diff --git a/.claude/settings.local.json b/.claude/settings.local.json index cce029b..542d1cb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,8 @@ "Bash(flutter test:*)", "Bash(flutter build:*)", "Bash(flutter clean:*)", - "Bash(dart analyze:*)" + "Bash(dart analyze:*)", + "Bash(dart test:*)" ], "deny": [], "ask": [] diff --git a/lib/core/utils/file_utils.dart b/lib/core/utils/file_utils.dart index 046c620..396eeaf 100644 --- a/lib/core/utils/file_utils.dart +++ b/lib/core/utils/file_utils.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart' as path_provider; import '../errors/app_error.dart'; import 'logger.dart'; @@ -12,7 +13,7 @@ class FileUtils { /// 获取应用文档目录 static Future getApplicationDocumentsDirectory() async { try { - final directory = await getApplicationDocumentsDirectory(); + final directory = await path_provider.getApplicationDocumentsDirectory(); Logger.debug('获取应用文档目录: ${directory.path}'); return directory; } catch (error, stackTrace) { @@ -28,7 +29,7 @@ class FileUtils { /// 获取临时目录 static Future getTemporaryDirectory() async { try { - final directory = await getTemporaryDirectory(); + final directory = await path_provider.getTemporaryDirectory(); Logger.debug('获取临时目录: ${directory.path}'); return directory; } catch (error, stackTrace) { @@ -44,7 +45,7 @@ class FileUtils { /// 获取应用支持目录(用于存储应用数据) static Future getApplicationSupportDirectory() async { try { - final directory = await getApplicationSupportDirectory(); + final directory = await path_provider.getApplicationSupportDirectory(); Logger.debug('获取应用支持目录: ${directory.path}'); return directory; } catch (error, stackTrace) { diff --git a/lib/core/utils/path_utils.dart b/lib/core/utils/path_utils.dart new file mode 100644 index 0000000..f847ffa --- /dev/null +++ b/lib/core/utils/path_utils.dart @@ -0,0 +1,322 @@ +import 'dart:io'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:uuid/uuid.dart'; +import '../errors/app_error.dart'; +import 'logger.dart'; + +/// 路径管理工具类 +/// 提供UUID生成、路径构建、文件命名等路径相关功能 +class PathUtils { + // 私有构造函数,防止实例化 + PathUtils._(); + + static const Uuid _uuid = Uuid(); + + /// 生成唯一ID(UUID v4) + /// 返回标准的UUID格式字符串 + static String generateUniqueId() { + return _uuid.v4(); + } + + /// 生成短格式唯一ID + /// 截取UUID前8位,适合文件名使用 + static String generateShortId() { + return _uuid.v4().substring(0, 8); + } + + /// 生成基于时间的唯一ID + /// 格式:时间戳_UUID,保证唯一性和可读性 + static String generateTimeBasedId() { + final timestamp = DateTime.now().millisecondsSinceEpoch; + final shortUuid = generateShortId(); + return '${timestamp}_$shortUuid'; + } + + /// 生成图片文件的唯一文件名 + /// [extension] 文件扩展名(包含点号,如 .jpg) + /// [prefix] 可选前缀,如 "IMG_" + static String generateImageFileName({ + required String extension, + String? prefix, + }) { + final baseName = generateTimeBasedId(); + final fileName = prefix != null ? '${prefix}$baseName' : baseName; + return '$fileName$extension'; + } + + /// 生成缩略图文件名 + /// 在原文件名基础上添加 _thumb 后缀 + static String generateThumbnailFileName(String originalFileName) { + final nameWithoutExt = path.basenameWithoutExtension(originalFileName); + final extension = path.extension(originalFileName); + return '${nameWithoutExt}_thumb$extension'; + } + + /// 构建日期分类的存储路径 + /// 按照 /yyyy/MM/dd/ 格式组织 + /// [basePath] 基础路径 + /// [date] 日期,默认为当前时间 + static String buildDateBasedPath({ + required String basePath, + DateTime? date, + }) { + final targetDate = date ?? DateTime.now(); + final year = targetDate.year.toString(); + final month = targetDate.month.toString().padLeft(2, '0'); + final day = targetDate.day.toString().padLeft(2, '0'); + + return path.join(basePath, year, month, day); + } + + /// 构建图片存储路径 + /// 返回完整的图片存储路径,包含日期分类 + static Future buildImageStoragePath({ + required String fileName, + DateTime? date, + }) async { + try { + final supportDir = await path_provider.getApplicationSupportDirectory(); + final imagesBasePath = path.join(supportDir.path, 'images'); + final dateBasedPath = buildDateBasedPath( + basePath: imagesBasePath, + date: date, + ); + + return path.join(dateBasedPath, fileName); + } catch (error, stackTrace) { + Logger.error('构建图片存储路径失败', error: error, stackTrace: stackTrace); + throw StorageError( + message: '构建图片存储路径失败: ${error.toString()}', + code: 'PATH_ERROR', + stackTrace: stackTrace, + ); + } + } + + /// 构建缩略图存储路径 + /// 返回完整的缩略图存储路径,包含日期分类 + static Future buildThumbnailStoragePath({ + required String fileName, + DateTime? date, + }) async { + try { + final supportDir = await path_provider.getApplicationSupportDirectory(); + final thumbnailsBasePath = path.join(supportDir.path, 'thumbnails'); + final dateBasedPath = buildDateBasedPath( + basePath: thumbnailsBasePath, + date: date, + ); + + return path.join(dateBasedPath, fileName); + } catch (error, stackTrace) { + Logger.error('构建缩略图存储路径失败', error: error, stackTrace: stackTrace); + throw StorageError( + message: '构建缩略图存储路径失败: ${error.toString()}', + code: 'PATH_ERROR', + stackTrace: stackTrace, + ); + } + } + + /// 获取文件的相对路径 + /// 相对于应用支持目录的路径 + static Future getRelativePath(String fullPath) async { + try { + final supportDir = await path_provider.getApplicationSupportDirectory(); + return path.relative(fullPath, from: supportDir.path); + } catch (error, stackTrace) { + Logger.error('获取相对路径失败', error: error, stackTrace: stackTrace); + throw StorageError( + message: '获取相对路径失败: ${error.toString()}', + code: 'PATH_ERROR', + stackTrace: stackTrace, + ); + } + } + + /// 从相对路径获取完整路径 + static Future getFullPath(String relativePath) async { + try { + final supportDir = await path_provider.getApplicationSupportDirectory(); + return path.normalize(path.join(supportDir.path, relativePath)); + } catch (error, stackTrace) { + Logger.error('获取完整路径失败', error: error, stackTrace: stackTrace); + throw StorageError( + message: '获取完整路径失败: ${error.toString()}', + code: 'PATH_ERROR', + stackTrace: stackTrace, + ); + } + } + + /// 检查路径是否安全 + /// 防止路径遍历攻击 + static bool isPathSafe(String pathToCheck) { + try { + // 检查是否包含路径遍历字符 + if (pathToCheck.contains('..') || pathToCheck.contains('~')) { + return false; + } + + // 检查是否为绝对路径 + if (path.isAbsolute(pathToCheck)) { + return false; + } + + // 检查路径是否规范化 + final normalizedPath = path.normalize(pathToCheck); + return normalizedPath == pathToCheck; + } catch (error) { + Logger.error('路径安全检查失败', error: error); + return false; + } + } + + /// 获取路径的父目录 + static String getParentDirectory(String filePath) { + return path.dirname(filePath); + } + + /// 检查文件扩展名是否有效 + static bool isValidFileExtension(String fileName, List validExtensions) { + final extension = path.extension(fileName).toLowerCase(); + return validExtensions.map((ext) => ext.toLowerCase()).contains(extension); + } + + /// 生成备份文件名 + /// 在原文件名基础上添加备份时间戳 + static String generateBackupFileName(String originalFileName) { + final nameWithoutExt = path.basenameWithoutExtension(originalFileName); + final extension = path.extension(originalFileName); + final timestamp = DateTime.now().millisecondsSinceEpoch; + return '${nameWithoutExt}_backup_$timestamp$extension'; + } + + /// 生成临时文件名 + /// 用于临时文件操作,格式:temp_UUID.扩展名 + static String generateTempFileName(String extension) { + final uuid = generateShortId(); + return 'temp_$uuid$extension'; + } + + /// 解析日期分类路径 + /// 从 /yyyy/MM/dd/ 格式的路径中提取日期信息 + static DateTime? parseDateFromPath(String dateBasedPath) { + try { + // 标准化路径分隔符 + final normalizedPath = path.normalize(dateBasedPath); + final pathParts = normalizedPath.split(path.separator); + + // 查找年、月、日的位置 + int yearIndex = -1, monthIndex = -1, dayIndex = -1; + + for (int i = 0; i < pathParts.length; i++) { + final part = pathParts[i]; + if (part.length == 4 && int.tryParse(part) != null && int.parse(part) > 1900 && int.parse(part) < 3000) { + yearIndex = i; + } else if (part.length == 2 && int.tryParse(part) != null && int.parse(part) >= 1 && int.parse(part) <= 12) { + if (monthIndex == -1) { + monthIndex = i; + } else if (dayIndex == -1) { + dayIndex = i; + } + } + } + + if (yearIndex != -1 && monthIndex != -1 && dayIndex != -1) { + final year = int.parse(pathParts[yearIndex]); + final month = int.parse(pathParts[monthIndex]); + final day = int.parse(pathParts[dayIndex]); + + return DateTime(year, month, day); + } + + return null; + } catch (error) { + Logger.error('解析日期路径失败', error: error); + return null; + } + } + + /// 获取路径的层级深度 + static int getPathDepth(String pathToCheck) { + return path.split(path.normalize(pathToCheck)).length; + } + + /// 检查两个路径是否有相同的父目录 + static bool hasSameParentDirectory(String path1, String path2) { + final parent1 = path.dirname(path.normalize(path1)); + final parent2 = path.dirname(path.normalize(path2)); + return parent1 == parent2; + } + + /// 生成安全的文件名 + /// 移除不安全的字符,确保文件名有效 + static String sanitizeFileName(String fileName) { + // 移除或替换不安全的字符 + final sanitized = fileName + .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_') + .replaceAll(RegExp(r'\s+'), '_') + .replaceAll(RegExp(r'_{2,}'), '_') + .trim(); + + // 确保文件名不为空 + if (sanitized.isEmpty) { + return 'unnamed_file'; + } + + return sanitized; + } +} + +/// 路径构建器类 +/// 提供链式调用的路径构建方式 +class PathBuilder { + final String _basePath; + final List _segments; + + PathBuilder(this._basePath) : _segments = []; + + /// 添加路径段 + PathBuilder add(String segment) { + _segments.add(segment); + return this; + } + + /// 添加日期段 + PathBuilder addDate(DateTime date) { + final year = date.year.toString(); + final month = date.month.toString().padLeft(2, '0'); + final day = date.day.toString().padLeft(2, '0'); + + _segments.addAll([year, month, day]); + return this; + } + + /// 添加UUID段 + PathBuilder addUuid() { + _segments.add(PathUtils.generateShortId()); + return this; + } + + /// 构建完整路径 + String build() { + if (_segments.isEmpty) { + return _basePath; + } + return path.join(_basePath, _segments.join(path.separator)); + } + + /// 创建目录 + Future createDirectory() async { + final fullPath = build(); + final directory = Directory(fullPath); + + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + return directory; + } +} \ No newline at end of file diff --git a/lib/data/datasources/local/database_migration.dart b/lib/data/datasources/local/database_migration.dart index e56e778..344e596 100644 --- a/lib/data/datasources/local/database_migration.dart +++ b/lib/data/datasources/local/database_migration.dart @@ -2,6 +2,7 @@ import 'package:hive/hive.dart'; import '../../models/hive_inspiration_image.dart'; import '../../models/hive_image_folder.dart'; import '../../models/hive_image_tag.dart'; +import '../../../core/utils/logger.dart'; /// 数据库迁移管理类 - 负责处理数据库版本升级和数据迁移 /// 确保应用在升级时能够正确迁移旧版本数据 @@ -120,8 +121,7 @@ class DatabaseMigration { await foldersBox.put(folder.id, folder); } else { - // TODO: 使用日志系统替代print - // print('默认文件夹已存在: ${folderConfig['name']}'); + Logger.debug('默认文件夹已存在: ${folderConfig['name']}'); } } } @@ -129,7 +129,7 @@ class DatabaseMigration { /// 创建默认标签 - 初始化系统常用的标签 /// [tagsBox] 标签数据盒 static Future createDefaultTags(Box tagsBox) async { - print('创建默认标签...'); + Logger.info('创建默认标签...'); // 默认标签配置 final defaultTags = [ @@ -219,8 +219,7 @@ class DatabaseMigration { Box foldersBox, Box tagsBox, ) async { - // TODO: 使用日志系统替代print - // print('开始数据库完整性验证...'); + Logger.info('开始数据库完整性验证...'); bool isValid = true; diff --git a/lib/data/datasources/local/hive_database.dart b/lib/data/datasources/local/hive_database.dart index 5b86c89..f6d7946 100644 --- a/lib/data/datasources/local/hive_database.dart +++ b/lib/data/datasources/local/hive_database.dart @@ -3,6 +3,7 @@ import 'package:path_provider/path_provider.dart' as path_provider; import '../../../data/models/hive_inspiration_image.dart'; import '../../../data/models/hive_image_folder.dart'; import '../../../data/models/hive_image_tag.dart'; +import '../../../core/utils/logger.dart'; import 'database_migration.dart'; /// Hive数据库管理类 - 负责本地数据存储和初始化 @@ -65,7 +66,7 @@ class HiveDatabase { if (currentDbVersion < _currentVersion) { try { - print('检测到数据库版本更新: $currentDbVersion -> $_currentVersion'); + Logger.info('检测到数据库版本更新: $currentDbVersion -> $_currentVersion'); // 使用专门的迁移管理类执行迁移 await DatabaseMigration.migrateFromVersion( @@ -76,15 +77,15 @@ class HiveDatabase { settingsBox, ); - print('数据库迁移完成'); + Logger.info('数据库迁移完成'); } catch (e) { - print('数据库迁移失败: $e'); + Logger.error('数据库迁移失败', error: e); // 如果迁移失败,尝试恢复到已知状态 await _handleMigrationFailure(currentDbVersion, e); } } else if (currentDbVersion > _currentVersion) { // 处理降级情况(通常不应该发生) - print('警告:数据库版本高于应用版本 ($currentDbVersion > $_currentVersion)'); + Logger.warning('数据库版本高于应用版本 ($currentDbVersion > $_currentVersion)'); await settingsBox.put(_versionKey, _currentVersion); } } @@ -93,7 +94,7 @@ class HiveDatabase { /// [fromVersion] 迁移起始版本 /// [error] 迁移过程中出现的错误 static Future _handleMigrationFailure(int fromVersion, dynamic error) async { - print('处理迁移失败,尝试恢复到安全状态...'); + Logger.error('处理迁移失败,尝试恢复到安全状态...', error: error); try { // 如果是最初版本(0)的迁移失败,可以尝试重新创建基础结构 @@ -107,7 +108,7 @@ class HiveDatabase { await settingsBox.put('last_migration_time', DateTime.now().toIso8601String()); } catch (recoveryError) { - print('迁移恢复失败: $recoveryError'); + Logger.fatal('迁移恢复失败', error: recoveryError); // 如果恢复也失败,只能抛出异常让上层处理 throw DatabaseMigrationException( fromVersion: fromVersion, @@ -132,7 +133,7 @@ class HiveDatabase { /// 创建紧急恢复数据 - 当迁移失败时的紧急恢复方案 /// 确保应用至少能正常运行,包含最基本的数据结构 static Future _createEmergencyData() async { - print('创建紧急恢复数据...'); + Logger.warning('创建紧急恢复数据...'); try { final foldersBox = Hive.box(_foldersBoxName);