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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user