沙盒修复

This commit is contained in:
Developer
2026-06-26 08:20:54 +08:00
parent 0f3fab70a7
commit 0c9faf30b7
2 changed files with 234 additions and 28 deletions

View File

@@ -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);
}
}
}