322 lines
9.9 KiB
Dart
322 lines
9.9 KiB
Dart
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<String> 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<String> 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<String> 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<String> 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<String> 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<String> _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<Directory> createDirectory() async {
|
||
final fullPath = build();
|
||
final directory = Directory(fullPath);
|
||
|
||
if (!await directory.exists()) {
|
||
await directory.create(recursive: true);
|
||
}
|
||
|
||
return directory;
|
||
}
|
||
} |