fix: 修复 FactoryReset 误删用户文件并将应用数据迁移至 Application Support
- 修复 macOS/Windows/Linux 非沙盒下 FactoryReset 递归删除 ~/Documents 导致用户项目源代码丢失的严重 bug,改为仅删除已知应用专属文件/子目录并增加路径安全校验 - 数据库文件从 getApplicationDocumentsDirectory() 迁移到 getApplicationSupportDirectory()(应用专属),含自动迁移逻辑 - 启用 macOS Debug 模式沙盒,使开发环境与生产环境路径行为一致 - 统一迁移 13 处应用数据存储位置(Hive、聊天附件、字体、稍后读同步等)到 Application Support,应用启动时执行一次性迁移 - backup_service.dart 备份文件迁移至 Application Support,getBackupList() 兼容扫描新旧两个路径并去重 - clearCache() 同样修复危险递归清空逻辑 详见 CHANGELOG.md v6.136.0 ~ v6.138.0
This commit is contained in:
@@ -52,7 +52,7 @@ class ChatAudioService {
|
||||
return null;
|
||||
}
|
||||
if (kIsWeb) throw UnsupportedError('Web端不支持文件系统');
|
||||
final appDocDir = await getApplicationDocumentsDirectory();
|
||||
final appDocDir = await getApplicationSupportDirectory();
|
||||
final audioDir = Directory('${appDocDir.path}/chat_audio');
|
||||
if (!await audioDir.exists()) {
|
||||
await audioDir.create(recursive: true);
|
||||
|
||||
@@ -24,7 +24,7 @@ class ChatFileService {
|
||||
|
||||
/// 获取聊天附件根目录
|
||||
static Future<Directory> getChatRootDir() async {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final appDir = await getApplicationSupportDirectory();
|
||||
final chatDir = Directory('${appDir.path}/chat_attachments');
|
||||
if (!chatDir.existsSync()) {
|
||||
chatDir.createSync(recursive: true);
|
||||
@@ -113,6 +113,9 @@ class ChatFileService {
|
||||
|
||||
/// 获取文件的绝对路径
|
||||
/// 如果传入的已经是绝对路径或网络地址,直接返回
|
||||
///
|
||||
/// 兼容新旧两个路径:优先使用 Application Support(新路径),
|
||||
/// 若文件不存在则回退到 Documents(旧路径,兼容迁移前残留文件)
|
||||
static Future<String> getAbsolutePath(String relativePath) async {
|
||||
if (relativePath.startsWith('http') || relativePath.startsWith('/')) {
|
||||
return relativePath;
|
||||
@@ -121,8 +124,28 @@ class ChatFileService {
|
||||
return relativePath;
|
||||
}
|
||||
if (kIsWeb) throw UnsupportedError('Web端不支持文件系统');
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
return '${appDir.path}/$relativePath';
|
||||
|
||||
// 优先使用新路径:Application Support
|
||||
try {
|
||||
final supportDir = await getApplicationSupportDirectory();
|
||||
final newPath = '${supportDir.path}/$relativePath';
|
||||
if (await File(newPath).exists()) {
|
||||
return newPath;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 回退到旧路径:Documents(兼容迁移前残留文件)
|
||||
try {
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
final oldPath = '${docDir.path}/$relativePath';
|
||||
if (await File(oldPath).exists()) {
|
||||
return oldPath;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 都不存在时返回新路径(让调用方处理不存在的情况)
|
||||
final supportDir = await getApplicationSupportDirectory();
|
||||
return '${supportDir.path}/$relativePath';
|
||||
}
|
||||
|
||||
/// 判断是否为沙箱相对路径(以chat_attachments/开头)
|
||||
|
||||
@@ -58,7 +58,7 @@ class CacheManagerService {
|
||||
];
|
||||
|
||||
Future<String> _getBasePath() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
return '${dir.path}${Platform.pathSeparator}file_transfer';
|
||||
}
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ class CloudCacheService {
|
||||
if (savePath != null) {
|
||||
outputPath = savePath;
|
||||
} else {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
outputPath =
|
||||
'${dir.path}${Platform.pathSeparator}cloud_cache${Platform.pathSeparator}${record.fileName}';
|
||||
await Directory(outputPath).parent.create(recursive: true);
|
||||
|
||||
@@ -178,7 +178,7 @@ class FontManagementNotifier extends Notifier<FontManagementState>
|
||||
Future<List<FontInfo>> _scanLocalFonts() async {
|
||||
if (pu.isWeb) return [];
|
||||
try {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final fontDir = Directory('${dir.path}/fonts');
|
||||
final List<FontInfo> fonts = [];
|
||||
|
||||
|
||||
@@ -582,23 +582,34 @@ class _MoreSettingsPageState extends ConsumerState<MoreSettingsPage> {
|
||||
Future<void> _safeClearAppFilesystemData() async {
|
||||
// ---- 6.1 删除数据库文件(精确文件,不递归父目录)----
|
||||
// 数据库连接已在步骤 4 关闭,文件可安全删除
|
||||
// 同时清理新旧两个路径:新路径为 getApplicationSupportDirectory(),
|
||||
// 旧路径为 getApplicationDocumentsDirectory()(兼容历史版本残留文件)
|
||||
final dbSearchDirs = <Directory>[];
|
||||
try {
|
||||
final docsDir = await getApplicationDocumentsDirectory();
|
||||
final docsPath = docsDir.path;
|
||||
// 仅删除明确的应用数据库文件,绝不遍历目录
|
||||
for (final suffix in ['', '-wal', '-shm', '-journal']) {
|
||||
final dbFile = File(p.join(docsPath, '$_appDbName$suffix'));
|
||||
if (await dbFile.exists()) {
|
||||
try {
|
||||
await dbFile.delete();
|
||||
Log.i('FactoryReset: 已删除 $_appDbName$suffix');
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 删除 $_appDbName$suffix 失败', e);
|
||||
dbSearchDirs.add(await getApplicationSupportDirectory());
|
||||
} catch (_) {}
|
||||
try {
|
||||
dbSearchDirs.add(await getApplicationDocumentsDirectory());
|
||||
} catch (_) {}
|
||||
|
||||
for (final dir in dbSearchDirs) {
|
||||
try {
|
||||
final dirPath = dir.path;
|
||||
// 仅删除明确的应用数据库文件,绝不遍历目录
|
||||
for (final suffix in ['', '-wal', '-shm', '-journal']) {
|
||||
final dbFile = File(p.join(dirPath, '$_appDbName$suffix'));
|
||||
if (await dbFile.exists()) {
|
||||
try {
|
||||
await dbFile.delete();
|
||||
Log.i('FactoryReset: 已删除 ${dir.path}/$_appDbName$suffix');
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 删除 $_appDbName$suffix 失败', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 数据库文件清理失败 (${dir.path})', e);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 数据库文件清理失败', e);
|
||||
}
|
||||
|
||||
// ---- 6.2 清理临时目录(应用专属,可安全清空内容)----
|
||||
@@ -646,7 +657,34 @@ class _MoreSettingsPageState extends ConsumerState<MoreSettingsPage> {
|
||||
// 部分平台可能不支持,忽略
|
||||
}
|
||||
|
||||
// ---- 6.5 清理 Flutter 图片缓存 ----
|
||||
// ---- 6.5 清理 Documents 目录下已知应用子目录(不删除根内容)----
|
||||
// 兼容迁移失败时残留的应用数据子目录
|
||||
// 永不递归删除 getApplicationDocumentsDirectory() 返回目录的根内容
|
||||
try {
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
const appSubdirs = [
|
||||
'image_cache',
|
||||
'readlater_sync',
|
||||
'chat_audio',
|
||||
'chat_attachments',
|
||||
'chat_trash',
|
||||
'cloud_cache',
|
||||
'file_transfer',
|
||||
'fonts',
|
||||
'xianyan_backups',
|
||||
];
|
||||
for (final subdirName in appSubdirs) {
|
||||
try {
|
||||
final subdir = Directory(p.join(docDir.path, subdirName));
|
||||
if (await subdir.exists()) {
|
||||
await subdir.delete(recursive: true);
|
||||
Log.i('FactoryReset: 已删除 Documents/$subdirName');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// ---- 6.6 清理 Flutter 图片缓存 ----
|
||||
try {
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
PaintingBinding.instance.imageCache.clearLiveImages();
|
||||
|
||||
@@ -974,37 +974,93 @@ class GeneralSettingsNotifier extends Notifier<GeneralSettingsState> {
|
||||
Future<void> clearCache() async {
|
||||
try {
|
||||
if (kIsWeb) return;
|
||||
final dirs = <String>[];
|
||||
|
||||
// 安全清理:避免递归删除 getApplicationDocumentsDirectory() 返回目录的根内容
|
||||
// 历史 bug:在 macOS/Windows/Linux 非沙盒下,该目录为用户公共 ~/Documents,
|
||||
// 递归删除会丢失用户项目源代码和其他文档
|
||||
const appBundleId = 'apps.xy.xianyan';
|
||||
|
||||
// 1. 清空临时目录(应用专属,安全)
|
||||
try {
|
||||
dirs.add((await getTemporaryDirectory()).path);
|
||||
} catch (_) {}
|
||||
try {
|
||||
dirs.add((await getApplicationDocumentsDirectory()).path);
|
||||
} catch (_) {}
|
||||
try {
|
||||
dirs.add((await getApplicationSupportDirectory()).path);
|
||||
final tmpDir = await getTemporaryDirectory();
|
||||
await _safeClearDirContents(tmpDir, '临时目录', requireAppId: false);
|
||||
} catch (_) {}
|
||||
|
||||
for (final path in dirs) {
|
||||
final dir = Directory(path);
|
||||
if (await dir.exists()) {
|
||||
await for (final entity in dir.list()) {
|
||||
try {
|
||||
if (entity is File) {
|
||||
await entity.delete();
|
||||
} else if (entity is Directory) {
|
||||
await entity.delete(recursive: true);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
// 2. 清空应用支持目录(应用专属,带路径校验)
|
||||
try {
|
||||
final supportDir = await getApplicationSupportDirectory();
|
||||
await _safeClearDirContents(supportDir, '应用支持目录',
|
||||
requireAppId: true, appId: appBundleId);
|
||||
} catch (_) {}
|
||||
|
||||
// 3. 清理 Documents 目录下已知应用子目录(不删除根内容)
|
||||
try {
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
const appSubdirs = [
|
||||
'image_cache',
|
||||
'readlater_sync',
|
||||
'chat_audio',
|
||||
'chat_attachments',
|
||||
'chat_trash',
|
||||
'cloud_cache',
|
||||
'file_transfer',
|
||||
'fonts',
|
||||
'xianyan_backups',
|
||||
];
|
||||
for (final subdirName in appSubdirs) {
|
||||
try {
|
||||
final subdir = Directory('${docDir.path}/$subdirName');
|
||||
if (await subdir.exists()) {
|
||||
await subdir.delete(recursive: true);
|
||||
Log.i('clearCache: 已删除 Documents/$subdirName');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
state = state.copyWith(general: state.general.copyWith(cacheSize: 0));
|
||||
Log.i('缓存已清除');
|
||||
} catch (e) {
|
||||
Log.e('缓存清除失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全清空目录内容(不删除目录本身)
|
||||
///
|
||||
/// [requireAppId] 为 true 时,校验目录路径是否包含应用标识,避免误删用户公共目录
|
||||
Future<void> _safeClearDirContents(
|
||||
Directory dir,
|
||||
String label, {
|
||||
required bool requireAppId,
|
||||
String appId = '',
|
||||
}) async {
|
||||
try {
|
||||
if (!await dir.exists()) return;
|
||||
|
||||
if (requireAppId) {
|
||||
final dirPath = dir.path.toLowerCase();
|
||||
final isAppSpecific = dirPath.contains(appId.toLowerCase()) ||
|
||||
dirPath.contains('xianyan');
|
||||
if (!isAppSpecific) {
|
||||
Log.w('clearCache: $label 路径未包含应用标识,跳过清理: ${dir.path}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await for (final entity in dir.list()) {
|
||||
try {
|
||||
if (entity is File) {
|
||||
await entity.delete();
|
||||
} else if (entity is Directory) {
|
||||
await entity.delete(recursive: true);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
Log.i('clearCache: $label 已清空');
|
||||
} catch (e) {
|
||||
Log.w('clearCache: 清理 $label 失败', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final generalSettingsProvider =
|
||||
|
||||
@@ -67,9 +67,9 @@ class FontDownloadService {
|
||||
if (pu.isWeb) throw UnsupportedError('Web端不支持字体管理');
|
||||
Directory dir;
|
||||
try {
|
||||
dir = await getApplicationDocumentsDirectory();
|
||||
dir = await getApplicationSupportDirectory();
|
||||
} catch (e) {
|
||||
Log.w('获取应用文档目录失败,降级使用临时目录', e);
|
||||
Log.w('获取应用支持目录失败,降级使用临时目录', e);
|
||||
dir = Directory.systemTemp;
|
||||
}
|
||||
final fontDir = Directory('${dir.path}/fonts');
|
||||
|
||||
Reference in New Issue
Block a user