沙盒修复
This commit is contained in:
@@ -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