沙盒修复
This commit is contained in:
69
CHANGELOG.md
69
CHANGELOG.md
@@ -6,6 +6,75 @@
|
||||
|
||||
***
|
||||
|
||||
## [v6.136.0] - 2026-06-26
|
||||
|
||||
### 🚨 严重 Bug 修复(数据丢失 - macOS/Windows/Linux 非沙盒模式)
|
||||
|
||||
#### 背景
|
||||
用户反馈:macOS Debug 模式下,进入「更多设置 → 重置与清理 → 清空软件所有数据」输入"重置"确认后,**整个项目源代码文件全部丢失**。
|
||||
|
||||
#### 根因
|
||||
`lib/features/settings/presentation/more_settings_page.dart` 的 `_executeFactoryReset` 方法第 6 步缓存清理逻辑存在严重缺陷:
|
||||
|
||||
```dart
|
||||
// ❌ 原 dangerous 代码
|
||||
final dirs = <Directory>[];
|
||||
dirs.add(await getTemporaryDirectory());
|
||||
dirs.add(await getApplicationDocumentsDirectory()); // macOS 非沙盒 → ~/Documents
|
||||
dirs.add(await getApplicationSupportDirectory());
|
||||
|
||||
for (final dir in dirs) {
|
||||
await for (final entity in dir.list()) {
|
||||
if (entity is Directory) {
|
||||
await entity.delete(recursive: true); // 递归删除子目录!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- macOS Debug 模式下 [DebugProfile.entitlements](file:///Users/wushu/Documents/trae_projects/project/xianyan/macos/Runner/DebugProfile.entitlements) 中 `com.apple.security.app-sandbox = false`(未启用沙盒)
|
||||
- 非沙盒模式下 `getApplicationDocumentsDirectory()` 返回**用户级 `~/Documents`**(不是应用容器目录)
|
||||
- 数据库连接 [native.dart#L27-L28](file:///Users/wushu/Documents/trae_projects/project/xianyan/lib/core/storage/database/database_connection/native.dart#L27-L28) 将 `xianyan.db` 放在 `~/Documents/xianyan.db`
|
||||
- 代码遍历 `~/Documents` 下所有子项并递归删除 → `~/Documents/trae_projects` 被递归删除 → **整个项目源代码丢失**
|
||||
- 同样的危险在 Windows/Linux 非沙盒下都存在
|
||||
|
||||
#### 修复方案
|
||||
**`lib/features/settings/presentation/more_settings_page.dart`**:
|
||||
|
||||
1. **新增 `_safeClearAppFilesystemData()` 方法**,遵循三大安全原则:
|
||||
- 永不递归删除 `getApplicationDocumentsDirectory()` 等返回目录的根内容
|
||||
- 仅删除已知应用专属路径(数据库文件、应用标识子目录)
|
||||
- 路径校验:清理子目录前验证路径包含 `apps.xy.xianyan` 或 `xianyan`,否则跳过
|
||||
|
||||
2. **精确删除数据库文件**:仅删除 `xianyan.db`、`xianyan.db-wal`、`xianyan.db-shm`、`xianyan.db-journal`,不遍历父目录
|
||||
|
||||
3. **`_safeClearDirectoryContents()` 通用方法**:
|
||||
- `requireAppIdentifier` 参数:要求路径包含应用标识才清理
|
||||
- 仅清空目录的直接子项,不删除目录本身
|
||||
- 单个文件删除失败不影响整体清理
|
||||
- 详细的日志记录
|
||||
|
||||
4. **分层清理策略**:
|
||||
- 数据库文件:精确文件删除(无条件)
|
||||
- 临时目录:直接清空(应用专属,安全)
|
||||
- 应用支持目录:带路径校验的清空
|
||||
- 应用缓存目录:直接清空(应用专属,安全)
|
||||
- Flutter ImageCache:内存缓存清理
|
||||
|
||||
#### 影响范围
|
||||
- ✅ macOS 非沙盒(Debug 模式):修复项目源代码丢失问题
|
||||
- ✅ Windows 非沙盒:修复 `Documents` 目录被清空问题
|
||||
- ✅ Linux 非沙盒:修复用户主目录被清空问题
|
||||
- ✅ iOS/Android 沙盒:原行为正常,现增加路径校验作为防御
|
||||
- ✅ macOS Release 沙盒模式:原行为正常,现增加路径校验作为防御
|
||||
|
||||
#### 举一反三
|
||||
- 数据库连接文件位置(`native.dart`)使用 `getApplicationDocumentsDirectory()` 在桌面非沙盒下不理想,建议未来迁移至 `getApplicationSupportDirectory()`(应用专属,更安全)
|
||||
- 「数据管理页面」的 `_clearAllData` 方法仅清理数据库表,不涉及文件系统删除,安全
|
||||
- 「缓存管理页面」的清理操作通过 `CacheService` 调用,使用应用专属路径,安全
|
||||
|
||||
---
|
||||
|
||||
## [v6.135.0] - 2026-06-26
|
||||
|
||||
### 🧹 仓库瘦身(历史大文件清理)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 更多设置页面
|
||||
/// 创建时间: 2026-05-08
|
||||
/// 更新时间: 2026-06-05
|
||||
/// 更新时间: 2026-06-26
|
||||
/// 作用: 内容搜索、存储管理、数据管理等设置
|
||||
/// 上次更新: Web兼容—path_provider调用添加kIsWeb保护
|
||||
/// 上次更新: 修复"清空软件数据"在 macOS/Windows/Linux 非沙盒模式下误删用户公共
|
||||
/// Documents 目录的严重 bug。原代码递归删除 getApplicationDocumentsDirectory()
|
||||
/// 返回目录的所有子项,在 macOS 非沙盒下会删除 ~/Documents 下所有内容
|
||||
/// (包括用户项目源代码)。现改为仅删除已知应用专属文件/子目录,并增加
|
||||
/// 路径安全校验,禁止删除用户公共目录。
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:io';
|
||||
@@ -14,6 +18,7 @@ import 'package:flutter/material.dart' show Divider;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -474,37 +479,24 @@ class _MoreSettingsPageState extends ConsumerState<MoreSettingsPage> {
|
||||
Log.w('FactoryReset: 本地数据库清除失败', e);
|
||||
}
|
||||
|
||||
// 6. 清除缓存文件(包括数据库文件)
|
||||
// 6. 清除应用专属缓存文件(安全清理:不删除用户公共目录)
|
||||
//
|
||||
// ⚠️ 历史 bug 修复(2026-06-26):
|
||||
// 原代码遍历 getApplicationDocumentsDirectory() 返回目录的所有子项并递归删除,
|
||||
// 在 macOS/Windows/Linux 非沙盒模式下,该路径是用户公共 ~/Documents 目录,
|
||||
// 递归删除会导致用户项目源代码、其他文档全部丢失。
|
||||
//
|
||||
// 修复策略:
|
||||
// - 仅删除已知应用专属文件(xianyan.db 及其 WAL/SHM 临时文件)
|
||||
// - 仅在路径被验证为应用专属时清理子目录内容
|
||||
// - 永不递归删除 getApplicationDocumentsDirectory() 返回目录的根内容
|
||||
try {
|
||||
if (kIsWeb) {
|
||||
// Web端无文件系统,跳过
|
||||
} else {
|
||||
final dirs = <Directory>[];
|
||||
try {
|
||||
dirs.add(await getTemporaryDirectory());
|
||||
} catch (_) {}
|
||||
try {
|
||||
dirs.add(await getApplicationDocumentsDirectory());
|
||||
} catch (_) {}
|
||||
try {
|
||||
dirs.add(await getApplicationSupportDirectory());
|
||||
} catch (_) {}
|
||||
|
||||
for (final dir in dirs) {
|
||||
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 (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
await _safeClearAppFilesystemData();
|
||||
}
|
||||
Log.i('FactoryReset: 缓存文件已清除');
|
||||
Log.i('FactoryReset: 应用专属缓存文件已清除');
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 缓存文件清除失败', e);
|
||||
}
|
||||
@@ -564,4 +556,149 @@ class _MoreSettingsPageState extends ConsumerState<MoreSettingsPage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 安全清理应用文件系统数据
|
||||
// ============================================================
|
||||
|
||||
/// 应用 bundle id(用于校验路径是否为应用专属)
|
||||
static const _appBundleId = 'apps.xy.xianyan';
|
||||
|
||||
/// 应用数据库名称(与 native.dart 中保持一致)
|
||||
static const _appDbName = 'xianyan.db';
|
||||
|
||||
/// 安全清理应用文件系统数据
|
||||
///
|
||||
/// 核心安全原则:
|
||||
/// 1. **永不递归删除** `getApplicationDocumentsDirectory()`、
|
||||
/// `getApplicationSupportDirectory()` 等返回目录的根内容
|
||||
/// - macOS/Windows/Linux 非沙盒模式下,这些路径可能是用户公共目录
|
||||
/// (如 `~/Documents`、`~/Library/Application Support`)
|
||||
/// - 递归删除会破坏用户其他文件和项目源代码
|
||||
/// 2. **仅删除已知应用专属路径**
|
||||
/// - 数据库文件:`<docs>/xianyan.db`、`<docs>/xianyan.db-wal`、`<docs>/xianyan.db-shm`
|
||||
/// - 应用专属子目录:路径中包含 bundle_id 或应用名的目录
|
||||
/// 3. **路径校验**:清理子目录前验证路径包含应用标识,否则跳过
|
||||
Future<void> _safeClearAppFilesystemData() async {
|
||||
// ---- 6.1 删除数据库文件(精确文件,不递归父目录)----
|
||||
// 数据库连接已在步骤 4 关闭,文件可安全删除
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 数据库文件清理失败', e);
|
||||
}
|
||||
|
||||
// ---- 6.2 清理临时目录(应用专属,可安全清空内容)----
|
||||
// getTemporaryDirectory() 在所有平台均返回应用专属缓存目录:
|
||||
// - Android: /data/data/<pkg>/cache 或 getCacheDir()
|
||||
// - iOS: <app>/Library/Caches
|
||||
// - macOS 沙盒: ~/Library/Containers/<bundle>/Data/Library/Caches
|
||||
// - macOS 非沙盒: ~/Library/Caches/<bundle>
|
||||
// - Windows: %TEMP% 或 <app>/cache
|
||||
// 因此清空其内容是安全的
|
||||
try {
|
||||
final tmpDir = await getTemporaryDirectory();
|
||||
await _safeClearDirectoryContents(tmpDir, label: '临时目录');
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 临时目录清理失败', e);
|
||||
}
|
||||
|
||||
// ---- 6.3 清理应用支持目录(带路径校验)----
|
||||
// getApplicationSupportDirectory() 各平台返回值:
|
||||
// - macOS 沙盒: ~/Library/Containers/<bundle>/Data/Library/Application Support
|
||||
// (应用专属,可安全清空内容)
|
||||
// - macOS 非沙盒: ~/Library/Application Support/<bundle>
|
||||
// (应用专属子目录,可安全清空内容)
|
||||
// - Windows: %APPDATA%/<vendor>/<app>(应用专属)
|
||||
// - Linux: ~/.local/share/<app>(应用专属)
|
||||
// - iOS: <app>/Library/Application Support(应用专属)
|
||||
// 即使如此,仍进行路径校验,仅当路径包含 bundle_id 或应用名时才清空
|
||||
try {
|
||||
final supportDir = await getApplicationSupportDirectory();
|
||||
await _safeClearDirectoryContents(
|
||||
supportDir,
|
||||
label: '应用支持目录',
|
||||
requireAppIdentifier: true,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 应用支持目录清理失败', e);
|
||||
}
|
||||
|
||||
// ---- 6.4 清理应用缓存目录(如果平台支持)----
|
||||
// getApplicationCacheDirectory() 是更明确的缓存目录,可安全清空
|
||||
try {
|
||||
final cacheDir = await getApplicationCacheDirectory();
|
||||
await _safeClearDirectoryContents(cacheDir, label: '应用缓存目录');
|
||||
} catch (e) {
|
||||
// 部分平台可能不支持,忽略
|
||||
}
|
||||
|
||||
// ---- 6.5 清理 Flutter 图片缓存 ----
|
||||
try {
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
PaintingBinding.instance.imageCache.clearLiveImages();
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: Flutter 图片缓存清理失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全清空目录内容(不删除目录本身)
|
||||
///
|
||||
/// [requireAppIdentifier] 为 true 时,会先校验目录路径是否包含应用标识
|
||||
/// (bundle_id 或应用名),若不包含则跳过清理,避免误删用户公共目录内容。
|
||||
///
|
||||
/// 该方法仅清空目录的**直接子项**,对子目录使用递归删除。
|
||||
/// 不会删除目录本身。
|
||||
Future<void> _safeClearDirectoryContents(
|
||||
Directory dir, {
|
||||
required String label,
|
||||
bool requireAppIdentifier = false,
|
||||
}) async {
|
||||
try {
|
||||
if (!await dir.exists()) return;
|
||||
|
||||
// 路径安全校验:若要求应用标识,检查路径是否包含 bundle_id 或应用名
|
||||
if (requireAppIdentifier) {
|
||||
final dirPath = dir.path.toLowerCase();
|
||||
final isAppSpecific = dirPath.contains(_appBundleId.toLowerCase()) ||
|
||||
dirPath.contains('xianyan');
|
||||
if (!isAppSpecific) {
|
||||
Log.w('FactoryReset: $label 路径未包含应用标识,跳过清理: ${dir.path}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var deletedCount = 0;
|
||||
await for (final entity in dir.list()) {
|
||||
try {
|
||||
if (entity is File) {
|
||||
await entity.delete();
|
||||
deletedCount++;
|
||||
} else if (entity is Directory) {
|
||||
await entity.delete(recursive: true);
|
||||
deletedCount++;
|
||||
}
|
||||
} catch (e) {
|
||||
// 单个文件删除失败不影响整体清理
|
||||
Log.w('FactoryReset: 删除 ${entity.path} 失败', e);
|
||||
}
|
||||
}
|
||||
Log.i('FactoryReset: $label 已清理 $deletedCount 个项目');
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 清理 $label 失败', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user