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:
Developer
2026-06-26 08:46:23 +08:00
parent 0c9faf30b7
commit 81ea0f60dc
20 changed files with 817 additions and 124 deletions

View File

@@ -1,9 +1,15 @@
/// ============================================================
/// 闲言APP — Drift原生数据库连接 (Android/iOS/macOS/Windows/Linux/OpenHarmony)
/// 创建时间: 2026-04-25
/// 更新时间: 2026-06-15
/// 更新时间: 2026-06-26
/// 作用: 原生平台数据库连接OpenHarmony 使用 sqflite_ohos 桥接
/// 上次更新: 移除sqlite3_flutter_libs依赖迁移至sqlite3包
/// 上次更新: 1. 数据库文件位置从 getApplicationDocumentsDirectory() 迁移到
/// getApplicationSupportDirectory()(应用专属目录)。
/// 原位置在 macOS/Windows/Linux 非沙盒模式下为用户公共 ~/Documents
/// 污染用户文档目录且存在被误删风险。新位置为应用专属目录,更安全。
/// 2. 已实现自动迁移:检测旧路径存在数据库文件时复制到新路径并删除旧文件,
/// 迁移失败时回退到旧路径保证数据可访问。
/// 3. 移除sqlite3_flutter_libs依赖迁移至sqlite3包
/// ============================================================
import 'dart:io';
@@ -17,6 +23,9 @@ import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu;
import 'ohos.dart';
/// 数据库文件名
const _kDbName = 'xianyan.db';
QueryExecutor openConnection() {
if (pu.isOhos) {
Log.i('Drift: 检测到 OpenHarmony 平台,使用 sqflite_ohos 后端');
@@ -24,9 +33,88 @@ QueryExecutor openConnection() {
}
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'xianyan.db'));
return NativeDatabase.createInBackground(file);
final dbFile = await _resolveDatabaseFile();
Log.i('Drift: 数据库路径 = ${dbFile.path}');
return NativeDatabase.createInBackground(dbFile);
});
}
/// 解析数据库文件位置,必要时执行旧路径 → 新路径迁移
///
/// **历史路径**`getApplicationDocumentsDirectory()/xianyan.db`
/// - macOS 非沙盒:`~/Documents/xianyan.db`(污染用户 Documents 目录)
/// - 桌面非沙盒:用户公共 Documents 目录(不安全,可能被误删)
///
/// **新路径**`getApplicationSupportDirectory()/xianyan.db`
/// - macOS 非沙盒:`~/Library/Application Support/apps.xy.xianyan/xianyan.db`(应用专属)
/// - macOS 沙盒:`~/Library/Containers/apps.xy.xianyan/Data/Library/Application Support/xianyan.db`
/// - iOS`<app>/Library/Application Support/xianyan.db`(应用专属)
/// - Windows`%APPDATA%/<vendor>/<app>/xianyan.db`(应用专属)
/// - Linux`~/.local/share/<app>/xianyan.db`(应用专属)
/// - Android`/data/data/<pkg>/files/xianyan.db`(与 Documents 相同路径)
///
/// **迁移策略**
/// 1. 新路径已存在 → 直接使用(迁移已完成或全新安装)
/// 2. 旧路径存在数据库文件 → 复制到新路径(含 -wal/-shm/-journal删除旧文件
/// 3. 都不存在 → 使用新路径(首次安装)
///
/// **安全性**
/// - 使用 copy 而非 rename复制成功后才删除旧文件避免迁移中断导致数据丢失
/// - 迁移失败时回退到旧路径,保证数据可访问
/// - 迁移在 LazyDatabase 初始化阶段执行,此时数据库未打开,无并发风险
Future<File> _resolveDatabaseFile() async {
// 新路径:应用专属支持目录
final newFolder = await getApplicationSupportDirectory();
final newFile = File(p.join(newFolder.path, _kDbName));
// 1. 新路径已存在,直接使用(迁移已完成或全新安装)
if (await newFile.exists()) {
return newFile;
}
// 2. 检查旧路径是否存在数据库文件
final oldFolder = await getApplicationDocumentsDirectory();
final oldFile = File(p.join(oldFolder.path, _kDbName));
// Android 上两个路径相同getApplicationSupportDirectory 在 Android 上
// 等同于 getApplicationDocumentsDirectory已被上面 newFile.exists() 拦截,
// 此处再防御性判断一次避免极端情况下重复处理
if (oldFile.path == newFile.path) {
return newFile;
}
if (await oldFile.exists()) {
Log.i('Database migration: 检测到旧路径数据库 ${oldFile.path},开始迁移');
try {
// 确保新目录存在
await newFolder.create(recursive: true);
// 复制主数据库文件
await oldFile.copy(newFile.path);
// 同时迁移 WAL/SHM/journal 辅助文件(若存在)
for (final suffix in ['-wal', '-shm', '-journal']) {
final oldAux = File(p.join(oldFolder.path, '$_kDbName$suffix'));
if (await oldAux.exists()) {
await oldAux.copy(p.join(newFolder.path, '$_kDbName$suffix'));
}
}
Log.i('Database migration: 数据库已从 ${oldFile.path} 迁移至 ${newFile.path}');
// 迁移成功后删除旧文件(删除失败不影响使用,仅记录警告)
try {
await oldFile.delete();
for (final suffix in ['-wal', '-shm', '-journal']) {
final oldAux = File(p.join(oldFolder.path, '$_kDbName$suffix'));
if (await oldAux.exists()) await oldAux.delete();
}
Log.i('Database migration: 旧路径文件已清理');
} catch (e) {
Log.w('Database migration: 旧文件删除失败(不影响使用)', e);
}
} catch (e) {
Log.e('Database migration: 迁移失败,继续使用旧路径', e);
return oldFile;
}
}
return newFile;
}

View File

@@ -51,37 +51,42 @@ class HiveSafeAccess {
if (_instance._hiveInitialized) return;
if (_instance._hiveInitFailed) return; // 已尝试过且失败,不再重试
// 方案1: 标准Hive.initFlutter()
// 方案1: 使用 Hive.init() + getApplicationSupportDirectory()
// 应用专属目录,避免污染用户 ~/Documents桌面非沙盒下尤为重要
// 注:原 Hive.initFlutter() 内部使用 getApplicationDocumentsDirectory()
// 在 macOS/Windows/Linux 非沙盒下会污染用户文档目录
try {
Log.i('[HiveSafe] 执行 Hive.initFlutter()...');
await Hive.initFlutter();
Log.i('[HiveSafe] 执行 Hive.init(applicationSupportPath)...');
if (kIsWeb) {
// Web 端 Hive.initFlutter 内部处理了路径,直接调用
await Hive.initFlutter();
} else {
final dir = await getApplicationSupportDirectory();
Hive.init(dir.path);
}
_instance._hiveInitialized = true;
Log.i('[HiveSafe] Hive 初始化完成');
return;
} catch (e) {
Log.w('[HiveSafe] Hive.initFlutter()失败,尝试降级方案', e);
Log.w('[HiveSafe] Hive.init(applicationSupportPath) 失败,尝试降级方案', e);
}
// 方案2: 降级使用Hive.init() + 手动获取路径绕过objective_c依赖
// Hive.initFlutter()内部调用getApplicationDocumentsDirectory()
// 在iOS模拟器上因objective_c库问题会失败这里手动获取路径并降级
// Web端: getApplicationDocumentsDirectory()不可用需kIsWeb保护
// 方案2: 降级使用 Hive.init() + getApplicationDocumentsDirectory()
// 在 iOS 模拟器上因 objective_c 库问题 getApplicationSupportDirectory() 可能失败
// 此时降级到 Documents 目录(沙盒内仍为应用专属,安全)
try {
Log.i('[HiveSafe] 降级方案: 使用Hive.init()手动指定路径...');
Log.i('[HiveSafe] 降级方案: 使用 Hive.init() + Documents...');
String hivePath;
try {
if (kIsWeb) {
// Web端不需要手动指定路径Hive.initFlutter()应该已经处理了
// 如果走到这里说明initFlutter也失败了Web端无法继续
throw UnsupportedError('Hive init failed on web');
}
final dir = await getApplicationDocumentsDirectory();
hivePath = dir.path;
} catch (_) {
if (kIsWeb) {
hivePath = '/tmp'; // Web端不会执行到这里但作为安全措施
hivePath = '/tmp';
} else {
// path_provider也不可用时使用系统临时目录
hivePath = Directory.systemTemp.path;
}
}