455 lines
14 KiB
Dart
455 lines
14 KiB
Dart
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';
|
|
|
|
/// 文件工具类
|
|
/// 提供文件存储、路径管理、存储空间检查等功能
|
|
class FileUtils {
|
|
// 私有构造函数,防止实例化
|
|
FileUtils._();
|
|
|
|
/// 获取应用文档目录
|
|
static Future<Directory> getApplicationDocumentsDirectory() async {
|
|
try {
|
|
final directory = await path_provider.getApplicationDocumentsDirectory();
|
|
Logger.debug('获取应用文档目录: ${directory.path}');
|
|
return directory;
|
|
} catch (error, stackTrace) {
|
|
Logger.error('获取应用文档目录失败', error: error, stackTrace: stackTrace);
|
|
throw StorageError(
|
|
message: '获取应用文档目录失败: ${error.toString()}',
|
|
code: 'STORAGE_ERROR',
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 获取临时目录
|
|
static Future<Directory> getTemporaryDirectory() async {
|
|
try {
|
|
final directory = await path_provider.getTemporaryDirectory();
|
|
Logger.debug('获取临时目录: ${directory.path}');
|
|
return directory;
|
|
} catch (error, stackTrace) {
|
|
Logger.error('获取临时目录失败', error: error, stackTrace: stackTrace);
|
|
throw StorageError(
|
|
message: '获取临时目录失败: ${error.toString()}',
|
|
code: 'STORAGE_ERROR',
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 获取应用支持目录(用于存储应用数据)
|
|
static Future<Directory> getApplicationSupportDirectory() async {
|
|
try {
|
|
final directory = await path_provider.getApplicationSupportDirectory();
|
|
Logger.debug('获取应用支持目录: ${directory.path}');
|
|
return directory;
|
|
} catch (error, stackTrace) {
|
|
Logger.error('获取应用支持目录失败', error: error, stackTrace: stackTrace);
|
|
throw StorageError(
|
|
message: '获取应用支持目录失败: ${error.toString()}',
|
|
code: 'STORAGE_ERROR',
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 创建应用存储目录结构
|
|
/// 按照日期格式创建目录:/yyyy/MM/dd/
|
|
static Future<String> createStorageDirectory(DateTime date) async {
|
|
try {
|
|
final supportDir = await getApplicationSupportDirectory();
|
|
|
|
// 构建日期路径
|
|
final year = date.year.toString();
|
|
final month = date.month.toString().padLeft(2, '0');
|
|
final day = date.day.toString().padLeft(2, '0');
|
|
|
|
final storagePath = path.join(supportDir.path, 'images', year, month, day);
|
|
final storageDir = Directory(storagePath);
|
|
|
|
// 如果目录不存在,创建目录
|
|
if (!await storageDir.exists()) {
|
|
await storageDir.create(recursive: true);
|
|
Logger.debug('创建存储目录: $storagePath');
|
|
}
|
|
|
|
return storagePath;
|
|
} catch (error, stackTrace) {
|
|
Logger.error('创建存储目录失败', error: error, stackTrace: stackTrace);
|
|
throw StorageError(
|
|
message: '创建存储目录失败: ${error.toString()}',
|
|
code: 'STORAGE_ERROR',
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 创建缩略图存储目录
|
|
static Future<String> createThumbnailDirectory(DateTime date) async {
|
|
try {
|
|
final supportDir = await getApplicationSupportDirectory();
|
|
|
|
// 构建日期路径
|
|
final year = date.year.toString();
|
|
final month = date.month.toString().padLeft(2, '0');
|
|
final day = date.day.toString().padLeft(2, '0');
|
|
|
|
final thumbnailPath = path.join(supportDir.path, 'thumbnails', year, month, day);
|
|
final thumbnailDir = Directory(thumbnailPath);
|
|
|
|
// 如果目录不存在,创建目录
|
|
if (!await thumbnailDir.exists()) {
|
|
await thumbnailDir.create(recursive: true);
|
|
Logger.debug('创建缩略图目录: $thumbnailPath');
|
|
}
|
|
|
|
return thumbnailPath;
|
|
} catch (error, stackTrace) {
|
|
Logger.error('创建缩略图目录失败', error: error, stackTrace: stackTrace);
|
|
throw StorageError(
|
|
message: '创建缩略图目录失败: ${error.toString()}',
|
|
code: 'STORAGE_ERROR',
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 创建临时文件目录
|
|
static Future<String> createTempDirectory() async {
|
|
try {
|
|
final tempDir = await getTemporaryDirectory();
|
|
final inspoSnapTempPath = path.join(tempDir.path, 'insposnap_temp');
|
|
final tempDirectory = Directory(inspoSnapTempPath);
|
|
|
|
if (!await tempDirectory.exists()) {
|
|
await tempDirectory.create(recursive: true);
|
|
Logger.debug('创建临时目录: $inspoSnapTempPath');
|
|
}
|
|
|
|
return inspoSnapTempPath;
|
|
} catch (error, stackTrace) {
|
|
Logger.error('创建临时目录失败', error: error, stackTrace: stackTrace);
|
|
throw StorageError(
|
|
message: '创建临时目录失败: ${error.toString()}',
|
|
code: 'STORAGE_ERROR',
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 检查存储空间是否充足
|
|
static Future<bool> hasEnoughStorageSpace({int requiredBytes = 100 * 1024 * 1024}) async {
|
|
try {
|
|
final directory = await getApplicationSupportDirectory();
|
|
// final stat = await FileStat.stat(directory.path);
|
|
|
|
// 获取可用空间(这里简化处理,实际需要更复杂的计算)
|
|
// 在实际应用中,可能需要使用 platform-specific 代码来获取准确的可用空间
|
|
|
|
// 模拟检查:如果目录存在且有写入权限,则认为空间充足
|
|
final testFile = File(path.join(directory.path, '.space_test'));
|
|
try {
|
|
await testFile.writeAsString('test');
|
|
await testFile.delete();
|
|
Logger.debug('存储空间检查通过');
|
|
return true;
|
|
} catch (e) {
|
|
Logger.warning('存储空间可能不足或没有写入权限');
|
|
return false;
|
|
}
|
|
} catch (error, stackTrace) {
|
|
Logger.error('检查存储空间失败', error: error, stackTrace: stackTrace);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 获取目录大小(字节)
|
|
static Future<int> getDirectorySize(Directory directory) async {
|
|
try {
|
|
int totalSize = 0;
|
|
|
|
if (!await directory.exists()) {
|
|
return 0;
|
|
}
|
|
|
|
await for (final entity in directory.list(recursive: true, followLinks: false)) {
|
|
if (entity is File) {
|
|
try {
|
|
final stat = await entity.stat();
|
|
totalSize += stat.size;
|
|
} catch (e) {
|
|
// 忽略单个文件的错误,继续统计其他文件
|
|
Logger.warning('获取文件大小失败: ${entity.path}');
|
|
}
|
|
}
|
|
}
|
|
|
|
Logger.debug('目录大小: ${directory.path} = ${(totalSize / 1024 / 1024).toStringAsFixed(2)}MB');
|
|
return totalSize;
|
|
} catch (error, stackTrace) {
|
|
Logger.error('获取目录大小失败', error: error, stackTrace: stackTrace);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// 删除目录及其所有内容
|
|
static Future<void> deleteDirectory(Directory directory) async {
|
|
try {
|
|
if (await directory.exists()) {
|
|
await directory.delete(recursive: true);
|
|
Logger.debug('删除目录: ${directory.path}');
|
|
}
|
|
} catch (error, stackTrace) {
|
|
Logger.error('删除目录失败', error: error, stackTrace: stackTrace);
|
|
throw StorageError(
|
|
message: '删除目录失败: ${error.toString()}',
|
|
code: 'STORAGE_ERROR',
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 清空目录内容(保留目录本身)
|
|
static Future<void> clearDirectory(Directory directory) async {
|
|
try {
|
|
if (!await directory.exists()) {
|
|
return;
|
|
}
|
|
|
|
await for (final entity in directory.list(followLinks: false)) {
|
|
try {
|
|
if (entity is File) {
|
|
await entity.delete();
|
|
} else if (entity is Directory) {
|
|
await entity.delete(recursive: true);
|
|
}
|
|
} catch (e) {
|
|
// 忽略单个文件/目录的删除错误
|
|
Logger.warning('删除失败: ${entity.path}');
|
|
}
|
|
}
|
|
|
|
Logger.debug('清空目录: ${directory.path}');
|
|
} catch (error, stackTrace) {
|
|
Logger.error('清空目录失败', error: error, stackTrace: stackTrace);
|
|
throw StorageError(
|
|
message: '清空目录失败: ${error.toString()}',
|
|
code: 'STORAGE_ERROR',
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 复制文件
|
|
static Future<File> copyFile(String sourcePath, String targetPath) async {
|
|
try {
|
|
final sourceFile = File(sourcePath);
|
|
if (!await sourceFile.exists()) {
|
|
throw StorageError(
|
|
message: '源文件不存在: $sourcePath',
|
|
code: 'STORAGE_ERROR',
|
|
);
|
|
}
|
|
|
|
// 确保目标目录存在
|
|
final targetDir = Directory(path.dirname(targetPath));
|
|
if (!await targetDir.exists()) {
|
|
await targetDir.create(recursive: true);
|
|
}
|
|
|
|
final targetFile = await sourceFile.copy(targetPath);
|
|
Logger.debug('复制文件: $sourcePath -> $targetPath');
|
|
return targetFile;
|
|
} catch (error, stackTrace) {
|
|
Logger.error('复制文件失败', error: error, stackTrace: stackTrace);
|
|
throw StorageError(
|
|
message: '复制文件失败: ${error.toString()}',
|
|
code: 'STORAGE_ERROR',
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 移动文件
|
|
static Future<File> moveFile(String sourcePath, String targetPath) async {
|
|
try {
|
|
final sourceFile = File(sourcePath);
|
|
if (!await sourceFile.exists()) {
|
|
throw StorageError(
|
|
message: '源文件不存在: $sourcePath',
|
|
code: 'STORAGE_ERROR',
|
|
);
|
|
}
|
|
|
|
// 确保目标目录存在
|
|
final targetDir = Directory(path.dirname(targetPath));
|
|
if (!await targetDir.exists()) {
|
|
await targetDir.create(recursive: true);
|
|
}
|
|
|
|
final targetFile = await sourceFile.rename(targetPath);
|
|
Logger.debug('移动文件: $sourcePath -> $targetPath');
|
|
return targetFile;
|
|
} catch (error, stackTrace) {
|
|
Logger.error('移动文件失败', error: error, stackTrace: stackTrace);
|
|
throw StorageError(
|
|
message: '移动文件失败: ${error.toString()}',
|
|
code: 'STORAGE_ERROR',
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 获取文件扩展名
|
|
static String getFileExtension(String filePath) {
|
|
return path.extension(filePath).toLowerCase();
|
|
}
|
|
|
|
/// 获取文件名(不含路径)
|
|
static String getFileName(String filePath) {
|
|
return path.basename(filePath);
|
|
}
|
|
|
|
/// 获取文件名(不含扩展名)
|
|
static String getFileNameWithoutExtension(String filePath) {
|
|
return path.basenameWithoutExtension(filePath);
|
|
}
|
|
|
|
/// 生成唯一的文件名
|
|
static String generateUniqueFileName(String originalFileName) {
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
|
final extension = getFileExtension(originalFileName);
|
|
final nameWithoutExt = getFileNameWithoutExtension(originalFileName);
|
|
return '${nameWithoutExt}_$timestamp$extension';
|
|
}
|
|
|
|
/// 检查文件是否存在
|
|
static Future<bool> fileExists(String filePath) async {
|
|
try {
|
|
final file = File(filePath);
|
|
return await file.exists();
|
|
} catch (error) {
|
|
Logger.error('检查文件存在失败', error: error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 获取文件大小(字节)
|
|
static Future<int> getFileSize(String filePath) async {
|
|
try {
|
|
final file = File(filePath);
|
|
if (await file.exists()) {
|
|
final stat = await file.stat();
|
|
return stat.size;
|
|
}
|
|
return 0;
|
|
} catch (error) {
|
|
Logger.error('获取文件大小失败', error: error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// 格式化文件大小
|
|
static String formatFileSize(int bytes) {
|
|
if (bytes <= 0) return '0 B';
|
|
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
int unitIndex = 0;
|
|
double size = bytes.toDouble();
|
|
|
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
size /= 1024;
|
|
unitIndex++;
|
|
}
|
|
|
|
return '${size.toStringAsFixed(2)} ${units[unitIndex]}';
|
|
}
|
|
|
|
/// 清理临时文件
|
|
static Future<void> cleanTempFiles() async {
|
|
try {
|
|
final tempDir = Directory(await createTempDirectory());
|
|
await clearDirectory(tempDir);
|
|
Logger.info('临时文件清理完成');
|
|
} catch (error, stackTrace) {
|
|
Logger.error('清理临时文件失败', error: error, stackTrace: stackTrace);
|
|
}
|
|
}
|
|
|
|
/// 获取存储使用情况
|
|
static Future<Map<String, dynamic>> getStorageUsage() async {
|
|
try {
|
|
final supportDir = await getApplicationSupportDirectory();
|
|
final imagesDir = Directory(path.join(supportDir.path, 'images'));
|
|
final thumbnailsDir = Directory(path.join(supportDir.path, 'thumbnails'));
|
|
|
|
final imagesSize = await getDirectorySize(imagesDir);
|
|
final thumbnailsSize = await getDirectorySize(thumbnailsDir);
|
|
final totalSize = imagesSize + thumbnailsSize;
|
|
|
|
return {
|
|
'imagesSize': imagesSize,
|
|
'thumbnailsSize': thumbnailsSize,
|
|
'totalSize': totalSize,
|
|
'imagesSizeFormatted': formatFileSize(imagesSize),
|
|
'thumbnailsSizeFormatted': formatFileSize(thumbnailsSize),
|
|
'totalSizeFormatted': formatFileSize(totalSize),
|
|
};
|
|
} catch (error, stackTrace) {
|
|
Logger.error('获取存储使用情况失败', error: error, stackTrace: stackTrace);
|
|
return {
|
|
'imagesSize': 0,
|
|
'thumbnailsSize': 0,
|
|
'totalSize': 0,
|
|
'imagesSizeFormatted': '0 B',
|
|
'thumbnailsSizeFormatted': '0 B',
|
|
'totalSizeFormatted': '0 B',
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 文件信息类
|
|
class FileInfo {
|
|
final String path;
|
|
final String name;
|
|
final String extension;
|
|
final int size;
|
|
final DateTime modifiedTime;
|
|
final bool exists;
|
|
|
|
FileInfo({
|
|
required this.path,
|
|
required this.name,
|
|
required this.extension,
|
|
required this.size,
|
|
required this.modifiedTime,
|
|
required this.exists,
|
|
});
|
|
|
|
factory FileInfo.fromFile(File file, [String? filePath]) {
|
|
final path = filePath ?? file.path;
|
|
final stat = file.statSync();
|
|
|
|
return FileInfo(
|
|
path: path,
|
|
name: file.uri.pathSegments.last,
|
|
extension: FileUtils.getFileExtension(path),
|
|
size: stat.size,
|
|
modifiedTime: stat.modified,
|
|
exists: stat.type != FileSystemEntityType.notFound,
|
|
);
|
|
}
|
|
|
|
String get sizeFormatted => FileUtils.formatFileSize(size);
|
|
|
|
@override
|
|
String toString() {
|
|
return 'FileInfo{path: $path, name: $name, size: $sizeFormatted, modified: $modifiedTime}';
|
|
}
|
|
} |