Compare commits
2 Commits
0f3fab70a7
...
81ea0f60dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81ea0f60dc | ||
|
|
0c9faf30b7 |
251
CHANGELOG.md
251
CHANGELOG.md
@@ -6,6 +6,257 @@
|
||||
|
||||
***
|
||||
|
||||
## [v6.138.0] - 2026-06-26
|
||||
|
||||
### 🔧 全量迁移应用数据至 Application Support(v6.137.0 后续)
|
||||
|
||||
#### 背景
|
||||
v6.137.0 完成了数据库文件和 Debug 沙盒的迁移,但项目中仍有 13 处使用 `getApplicationDocumentsDirectory()` 存储应用内部数据(Hive、聊天文件、字体等)。本次统一迁移所有应用内部数据到 `getApplicationSupportDirectory()`,并修复 `clearCache()` 中与 FactoryReset 同样的危险清空逻辑。
|
||||
|
||||
#### 新增工具函数
|
||||
**`lib/core/utils/platform/path_provider_native.dart`**:
|
||||
- 新增 `getAppDataDirectory()`:返回 `getApplicationSupportDirectory()`,推荐用于应用内部数据
|
||||
- 新增 `migrateAppDataFromDocumentsToSupport()`:一次性迁移函数
|
||||
- 迁移已知应用子目录:`image_cache`、`readlater_sync`、`chat_audio`、`chat_attachments`、`chat_trash`、`cloud_cache`、`file_transfer`、`fonts`、`backups`
|
||||
- 迁移 Hive 文件:`*.hive`、`*.lock`
|
||||
- 使用 copy 而非 rename,复制成功后才删除旧文件
|
||||
- 若新路径已存在同名文件/目录,跳过(避免覆盖)
|
||||
- 进程内缓存迁移状态,避免重复执行
|
||||
|
||||
**`lib/core/utils/platform/platform_utils.dart`**:
|
||||
- 新增 `safeAppDataPath` getter:暴露 `getAppDataDirectory()`
|
||||
- 新增 `migrateAppData()` 函数:暴露迁移逻辑
|
||||
|
||||
**`lib/main.dart`**:
|
||||
- 在 `KvStorage.init()` 之前调用 `pu.migrateAppData()`,确保存储初始化时使用新路径
|
||||
|
||||
#### 替换的使用点(13 处)
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `hive_safe_access.dart` | Hive.init 从 `Hive.initFlutter()` 改为 `Hive.init(applicationSupportPath)`,降级方案保留 Documents |
|
||||
| `image_cache_metadata_service.dart` | 索引重建扫描新路径,兼容旧路径残留 |
|
||||
| `readlater_device_sync_service.dart` | 同步文件保存到新路径,扫描兼容新旧路径 |
|
||||
| `chat_audio_service.dart` | 录音文件存储到新路径 |
|
||||
| `chat_file_service.dart` | 附件存储到新路径,路径解析兼容新旧 |
|
||||
| `cloud_cache_service.dart` | 云端缓存存储到新路径 |
|
||||
| `cache_manager_service.dart` | 文件传输缓存存储到新路径 |
|
||||
| `font_download_service.dart` | 字体文件存储到新路径 |
|
||||
| `font_management_notifier.dart` | 字体扫描新路径 |
|
||||
|
||||
#### 修复 clearCache() 危险清空逻辑
|
||||
**`lib/features/settings/providers/general_settings_provider.dart`**:
|
||||
|
||||
原 `clearCache()` 方法存在与 FactoryReset 同样的危险逻辑:遍历 `getApplicationDocumentsDirectory()` 返回目录的所有子项并递归删除。在 macOS/Windows/Linux 非沙盒下会删除用户 `~/Documents` 下所有内容。
|
||||
|
||||
修复方案:
|
||||
1. **临时目录**:直接清空内容(应用专属,安全)
|
||||
2. **Application Support**:带路径校验的清空(要求路径包含 `apps.xy.xianyan` 或 `xianyan`)
|
||||
3. **Documents**:仅清理已知应用子目录(`image_cache`、`chat_audio`、`chat_attachments` 等 9 个),不删除根内容
|
||||
4. 新增 `_safeClearDirContents()` 辅助方法,支持路径校验
|
||||
|
||||
#### 保留的使用点(用户可见文件)
|
||||
以下使用点保留 `getApplicationDocumentsDirectory()`,因为存储的是用户可见文件:
|
||||
- `data_management_export_mixin.dart`:数据导出 `.xypk` 文件(导出后通过 share_plus 分享,路径不重要临时文件)
|
||||
|
||||
#### backup_service.dart 备份迁移(补充)
|
||||
**`lib/core/services/data/backup_service.dart`**:
|
||||
- `backupDirPath` 从 `safeAppDirPath`(Documents)改为 `safeAppDataPath`(Application Support)
|
||||
- `getBackupList()` 兼容新旧两个路径:同时扫描 Application Support 和 Documents,合并结果并按文件名去重
|
||||
- 新增 `_scanBackupDir()` 私有方法,提取目录扫描逻辑
|
||||
|
||||
**`lib/core/utils/platform/path_provider_native.dart`**:
|
||||
- 修正迁移子目录列表:`backups` → `xianyan_backups`(与 `backup_service.dart` 的 `_backupDirName` 保持一致)
|
||||
|
||||
**`lib/features/settings/presentation/more_settings_page.dart`** 和
|
||||
**`lib/features/settings/providers/general_settings_provider.dart`**:
|
||||
- Documents 目录清理列表新增 `xianyan_backups` 子目录
|
||||
- FactoryReset 的 `_safeClearAppFilesystemData()` 新增步骤 6.5:清理 Documents 下已知应用子目录
|
||||
|
||||
#### 验证
|
||||
- `flutter analyze` 所有修改文件 → No issues found
|
||||
- 迁移逻辑在应用启动时自动执行,用户无感知
|
||||
- 兼容旧路径残留文件,迁移失败不影响使用
|
||||
|
||||
---
|
||||
|
||||
## [v6.137.0] - 2026-06-26
|
||||
|
||||
### 🔧 数据库路径迁移 + Debug 沙盒启用(v6.136.0 后续优化)
|
||||
|
||||
#### 背景
|
||||
v6.136.0 修复了 FactoryReset 误删项目源代码的严重 bug,但暴露出两个深层问题:
|
||||
1. 数据库文件位置(`getApplicationDocumentsDirectory()`)在桌面非沙盒下不理想
|
||||
2. Debug 模式未启用沙盒,导致开发环境与生产环境路径行为不一致(这是 bug 复现的根源)
|
||||
|
||||
本版本实施两个优化建议,从根因层面避免类似问题再次发生。
|
||||
|
||||
---
|
||||
|
||||
### 建议 1:数据库文件迁移至应用专属目录
|
||||
|
||||
#### 修改文件
|
||||
**`lib/core/storage/database/database_connection/native.dart`**:
|
||||
|
||||
#### 路径变更
|
||||
|
||||
| 平台 | 旧路径 | 新路径 |
|
||||
|------|--------|--------|
|
||||
| macOS 非沙盒 | `~/Documents/xianyan.db` | `~/Library/Application Support/apps.xy.xianyan/xianyan.db` |
|
||||
| macOS 沙盒 | `~/Library/Containers/.../Data/Documents/xianyan.db` | `~/Library/Containers/.../Data/Library/Application Support/xianyan.db` |
|
||||
| iOS | `<app>/Documents/xianyan.db` | `<app>/Library/Application Support/xianyan.db` |
|
||||
| Windows | `~/Documents/xianyan.db` | `%APPDATA%/<vendor>/<app>/xianyan.db` |
|
||||
| Linux | `~/Documents/xianyan.db` | `~/.local/share/<app>/xianyan.db` |
|
||||
| Android | `/data/data/<pkg>/files/xianyan.db` | `/data/data/<pkg>/files/xianyan.db`(路径相同,无影响) |
|
||||
|
||||
#### 自动迁移逻辑
|
||||
新增 `_resolveDatabaseFile()` 方法,在 `LazyDatabase` 初始化阶段执行:
|
||||
|
||||
1. **新路径已存在** → 直接使用(迁移已完成或全新安装)
|
||||
2. **旧路径存在数据库文件** → 复制到新路径(含 `-wal`/`-shm`/`-journal` 辅助文件),删除旧文件
|
||||
3. **都不存在** → 使用新路径(首次安装)
|
||||
4. **迁移失败** → 回退到旧路径,保证数据可访问
|
||||
|
||||
#### 安全性保障
|
||||
- 使用 `copy` 而非 `rename`,复制成功后才删除旧文件,避免迁移中断导致数据丢失
|
||||
- 迁移在 `LazyDatabase` 初始化阶段执行,此时数据库未打开,无并发风险
|
||||
- 详细的日志记录迁移过程,便于排查问题
|
||||
|
||||
#### 联动修改
|
||||
**`lib/features/settings/presentation/more_settings_page.dart`**:
|
||||
- `_safeClearAppFilesystemData()` 方法第 6.1 步扩展:同时清理新旧两个路径下的数据库文件
|
||||
- 新路径:`getApplicationSupportDirectory()/xianyan.db`
|
||||
- 旧路径:`getApplicationDocumentsDirectory()/xianyan.db`(兼容历史版本残留)
|
||||
|
||||
---
|
||||
|
||||
### 建议 2:启用 Debug 沙盒
|
||||
|
||||
#### 修改文件
|
||||
**`macos/Runner/DebugProfile.entitlements`**:
|
||||
- `com.apple.security.app-sandbox`:`false` → `true`
|
||||
|
||||
#### 决策过程
|
||||
启用 Debug 沙盒与建议 1 存在冲突:
|
||||
- 沙盒内应用**无法访问沙盒外路径**
|
||||
- 若先迁移数据库到沙盒外的 `~/Library/Application Support/apps.xy.xianyan/`,再启用沙盒,迁移逻辑会尝试从沙盒外旧路径复制,但沙盒内应用无权访问 → 迁移失败
|
||||
|
||||
经用户确认,选择"启用 Debug 沙盒 + 接受数据丢失"方案:
|
||||
- ✅ 项目源代码不会丢失(IDE 进程不受沙盒影响)
|
||||
- ⚠️ Debug 模式下应用内数据(数据库、收藏、历史等)会丢失,开发者需重新登录/创建
|
||||
- ✅ 长期收益:开发环境与生产环境路径行为一致,避免类似 bug 复现
|
||||
|
||||
#### 沙盒启用后路径变化(Debug 模式)
|
||||
|
||||
| API | 启用前(非沙盒) | 启用后(沙盒) |
|
||||
|-----|------------------|----------------|
|
||||
| `getApplicationDocumentsDirectory()` | `~/Documents` | `~/Library/Containers/apps.xy.xianyan/Data/Documents` |
|
||||
| `getApplicationSupportDirectory()` | `~/Library/Application Support/apps.xy.xianyan` | `~/Library/Containers/apps.xy.xianyan/Data/Library/Application Support` |
|
||||
| `getTemporaryDirectory()` | `~/Library/Caches/apps.xy.xianyan` | `~/Library/Containers/apps.xy.xianyan/Data/Library/Caches` |
|
||||
|
||||
#### 影响范围评估
|
||||
- ✅ Hive 存储:迁移到沙盒内 Documents,安全
|
||||
- ✅ 图片缓存元数据:迁移到沙盒内 Documents,安全
|
||||
- ✅ 聊天音频/文件:迁移到沙盒内 Documents,安全
|
||||
- ✅ 字体下载服务:迁移到沙盒内 Documents,安全
|
||||
- ✅ LocalSend 文件接收:存储在应用专属目录,不依赖 `~/Downloads`,安全
|
||||
- ✅ 稍后读同步:迁移到沙盒内 Documents,安全
|
||||
- ✅ Flutter 调试:Dart VM Service 通过 localhost 通信,沙盒允许 localhost 网络访问
|
||||
|
||||
#### 用户须知
|
||||
- Debug 模式下首次启动会显示空数据库,需重新登录账户
|
||||
- 旧 Debug 数据库文件残留在沙盒外路径,可手动清理:
|
||||
- `~/Documents/xianyan.db`
|
||||
- `~/Library/Application Support/apps.xy.xianyan/xianyan.db`
|
||||
|
||||
---
|
||||
|
||||
### 举一反三
|
||||
1. **其他使用 `getApplicationDocumentsDirectory()` 的位置**:项目中仍有 13 处使用此 API 存储应用数据(Hive、图片缓存、聊天文件、字体、稍后读等)。本次未一并迁移,因为:
|
||||
- 启用沙盒后,这些路径自动变为应用专属(沙盒内 Documents),不再有污染问题
|
||||
- 桌面非沙盒场景下仍有潜在风险,但优先级低于数据库
|
||||
- 后续可考虑统一迁移到 `getApplicationSupportDirectory()`
|
||||
|
||||
2. **数据库迁移日志监控**:建议在 Release 模式下监控 `Database migration:` 日志,了解用户迁移情况,及时发现问题
|
||||
|
||||
3. **沙盒启用后的回归测试**:建议在 Debug 模式下测试以下功能:
|
||||
- 数据库创建/读写
|
||||
- 文件下载(字体、图片)
|
||||
- LocalSend 文件接收
|
||||
- 相机/麦克风权限
|
||||
- 定位功能
|
||||
|
||||
---
|
||||
|
||||
## [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,11 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 自动备份服务
|
||||
/// 创建时间: 2026-05-04
|
||||
/// 更新时间: 2026-06-05
|
||||
/// 更新时间: 2026-06-26
|
||||
/// 作用: 定时自动备份用户数据到本地,支持备份管理和恢复
|
||||
/// 上次更新: 完善Web端守卫,所有公共方法添加isWeb早期返回
|
||||
/// 上次更新: 备份存储路径从 getApplicationDocumentsDirectory() 迁移到
|
||||
/// getApplicationSupportDirectory()(应用专属目录),避免桌面非沙盒
|
||||
/// 下污染用户 ~/Documents 目录。getBackupList() 兼容旧路径残留文件。
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:convert';
|
||||
@@ -75,12 +77,18 @@ class BackupService {
|
||||
return DateTime.now().difference(last) >= interval;
|
||||
}
|
||||
|
||||
/// 备份目录路径(应用专属:Application Support)
|
||||
///
|
||||
/// 历史路径为 getApplicationDocumentsDirectory()/xianyan_backups,
|
||||
/// 在 macOS/Windows/Linux 非沙盒下会污染用户 ~/Documents 目录。
|
||||
/// 现迁移到 getApplicationSupportDirectory()/xianyan_backups(应用专属)。
|
||||
/// 应用启动时 migrateAppData() 会自动迁移旧路径下的备份文件。
|
||||
static Future<String> get backupDirPath async {
|
||||
if (pu.isWeb) throw UnsupportedError('Web端不支持文件系统操作');
|
||||
var dirPath = await pu.safeAppDirPath;
|
||||
// 无法获取应用文档目录时降级使用临时目录(如iOS模拟器某些环境)
|
||||
var dirPath = await pu.safeAppDataPath;
|
||||
// 无法获取应用支持目录时降级使用临时目录(如iOS模拟器某些环境)
|
||||
if (dirPath == null) {
|
||||
Log.w('BackupService: 无法获取应用文档目录,降级使用临时目录');
|
||||
Log.w('BackupService: 无法获取应用支持目录,降级使用临时目录');
|
||||
dirPath = await pu.safeTempDirPath;
|
||||
if (dirPath == null) {
|
||||
throw UnsupportedError('无法获取任何可用目录');
|
||||
@@ -93,38 +101,83 @@ class BackupService {
|
||||
return backupDir.path;
|
||||
}
|
||||
|
||||
/// 获取备份列表(兼容新旧两个路径)
|
||||
///
|
||||
/// 同时扫描 Application Support(新路径,主要)和 Documents(旧路径,兼容),
|
||||
/// 合并结果并按创建时间降序排序。旧路径扫描用于兼容迁移失败时的残留文件。
|
||||
static Future<List<BackupFileInfo>> getBackupList() async {
|
||||
if (pu.isWeb) return [];
|
||||
final dirPath = await backupDirPath;
|
||||
final dir = Directory(dirPath);
|
||||
final files = await dir
|
||||
.list()
|
||||
.where(
|
||||
(f) =>
|
||||
f is File &&
|
||||
f.path.endsWith(_backupExtension) &&
|
||||
File(f.path).path.split('/').last.startsWith(_backupFilePrefix),
|
||||
)
|
||||
.cast<File>()
|
||||
.toList();
|
||||
|
||||
final backups = <BackupFileInfo>[];
|
||||
for (final file in files) {
|
||||
final stat = await file.stat();
|
||||
final fileName = file.path.split(Platform.pathSeparator).last;
|
||||
final sizeBytes = await file.length();
|
||||
backups.add(
|
||||
BackupFileInfo(
|
||||
path: file.path,
|
||||
fileName: fileName,
|
||||
createdAt: stat.modified,
|
||||
sizeBytes: sizeBytes,
|
||||
),
|
||||
);
|
||||
|
||||
// 1. 扫描新路径:Application Support(主要)
|
||||
try {
|
||||
final newDirPath = await backupDirPath;
|
||||
backups.addAll(await _scanBackupDir(newDirPath));
|
||||
} catch (e) {
|
||||
Log.w('BackupService: 扫描新路径备份失败', e);
|
||||
}
|
||||
|
||||
backups.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return backups;
|
||||
// 2. 扫描旧路径:Documents(兼容迁移失败时的残留文件)
|
||||
try {
|
||||
final oldDirPath = await pu.safeAppDirPath;
|
||||
if (oldDirPath != null) {
|
||||
final legacyDir = Directory('$oldDirPath/$_backupDirName');
|
||||
if (await legacyDir.exists()) {
|
||||
backups.addAll(await _scanBackupDir(legacyDir.path));
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 去重(同一文件名只保留一个,优先新路径)
|
||||
final seen = <String>{};
|
||||
final deduped = <BackupFileInfo>[];
|
||||
for (final backup in backups) {
|
||||
if (seen.add(backup.fileName)) {
|
||||
deduped.add(backup);
|
||||
}
|
||||
}
|
||||
|
||||
deduped.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return deduped;
|
||||
}
|
||||
|
||||
/// 扫描指定目录下的备份文件
|
||||
static Future<List<BackupFileInfo>> _scanBackupDir(String dirPath) async {
|
||||
try {
|
||||
final dir = Directory(dirPath);
|
||||
if (!await dir.exists()) return [];
|
||||
|
||||
final files = await dir
|
||||
.list()
|
||||
.where(
|
||||
(f) =>
|
||||
f is File &&
|
||||
f.path.endsWith(_backupExtension) &&
|
||||
File(f.path).path.split('/').last.startsWith(_backupFilePrefix),
|
||||
)
|
||||
.cast<File>()
|
||||
.toList();
|
||||
|
||||
final backups = <BackupFileInfo>[];
|
||||
for (final file in files) {
|
||||
final stat = await file.stat();
|
||||
final fileName = file.path.split(Platform.pathSeparator).last;
|
||||
final sizeBytes = await file.length();
|
||||
backups.add(
|
||||
BackupFileInfo(
|
||||
path: file.path,
|
||||
fileName: fileName,
|
||||
createdAt: stat.modified,
|
||||
sizeBytes: sizeBytes,
|
||||
),
|
||||
);
|
||||
}
|
||||
return backups;
|
||||
} catch (e) {
|
||||
Log.w('BackupService: 扫描备份目录失败: $dirPath', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> performBackup() async {
|
||||
|
||||
@@ -470,15 +470,31 @@ class ImageCacheMetadataService {
|
||||
}
|
||||
}
|
||||
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
final docImageDir = Directory('${docDir.path}/image_cache');
|
||||
if (await docImageDir.exists()) {
|
||||
await for (final entity in docImageDir.list(recursive: true)) {
|
||||
if (entity is File) {
|
||||
await indexFile(entity.path);
|
||||
// 应用数据目录(新路径:Application Support,主要)
|
||||
try {
|
||||
final supportDir = await getApplicationSupportDirectory();
|
||||
final supportImageDir = Directory('${supportDir.path}/image_cache');
|
||||
if (await supportImageDir.exists()) {
|
||||
await for (final entity in supportImageDir.list(recursive: true)) {
|
||||
if (entity is File) {
|
||||
await indexFile(entity.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 旧路径兼容:Documents/image_cache(迁移失败时仍有残留文件)
|
||||
try {
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
final docImageDir = Directory('${docDir.path}/image_cache');
|
||||
if (await docImageDir.exists()) {
|
||||
await for (final entity in docImageDir.list(recursive: true)) {
|
||||
if (entity is File) {
|
||||
await indexFile(entity.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
Log.i('ImageCacheMetadataService: 索引重建完成');
|
||||
}
|
||||
|
||||
@@ -122,20 +122,40 @@ class ReadlaterDeviceSyncService {
|
||||
}
|
||||
|
||||
/// 扫描传输目录中未处理的稍后读同步文件
|
||||
///
|
||||
/// 同时扫描新旧两个路径(Application Support + Documents),
|
||||
/// 兼容迁移前残留的同步文件。
|
||||
Future<int> scanAndImportPendingSyncFiles() async {
|
||||
try {
|
||||
if (kIsWeb) return 0;
|
||||
final appDocDir = await getApplicationDocumentsDirectory();
|
||||
final syncDir = Directory('${appDocDir.path}/$_syncDirName');
|
||||
if (!await syncDir.exists()) return 0;
|
||||
|
||||
int totalImported = 0;
|
||||
await for (final entity in syncDir.list()) {
|
||||
if (entity is File && entity.path.contains(_syncFilePrefix)) {
|
||||
final imported = await _importFromFile(entity);
|
||||
totalImported += imported;
|
||||
|
||||
// 新路径:Application Support(主要)
|
||||
try {
|
||||
final supportDir = await getApplicationSupportDirectory();
|
||||
final syncDir = Directory('${supportDir.path}/$_syncDirName');
|
||||
if (await syncDir.exists()) {
|
||||
await for (final entity in syncDir.list()) {
|
||||
if (entity is File && entity.path.contains(_syncFilePrefix)) {
|
||||
totalImported += await _importFromFile(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 旧路径兼容:Documents(迁移失败时仍有残留文件)
|
||||
try {
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
final syncDir = Directory('${docDir.path}/$_syncDirName');
|
||||
if (await syncDir.exists()) {
|
||||
await for (final entity in syncDir.list()) {
|
||||
if (entity is File && entity.path.contains(_syncFilePrefix)) {
|
||||
totalImported += await _importFromFile(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (totalImported > 0) {
|
||||
Log.i('ReadlaterSync: 扫描导入 $totalImported 条稍后读消息');
|
||||
@@ -184,7 +204,7 @@ class ReadlaterDeviceSyncService {
|
||||
/// 保存同步数据为临时文件
|
||||
Future<String?> _saveSyncFile(Map<String, dynamic> syncData) async {
|
||||
try {
|
||||
final appDocDir = await getApplicationDocumentsDirectory();
|
||||
final appDocDir = await getApplicationSupportDirectory();
|
||||
final syncDir = Directory('${appDocDir.path}/$_syncDirName');
|
||||
if (!await syncDir.exists()) {
|
||||
await syncDir.create(recursive: true);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — path_provider 原生实现
|
||||
/// 创建时间: 2026-05-20
|
||||
/// 更新时间: 2026-05-20
|
||||
/// 作用: 原生平台使用path_provider获取目录路径
|
||||
/// 上次更新: 初始创建
|
||||
/// 更新时间: 2026-06-26
|
||||
/// 作用: 原生平台使用 path_provider 获取目录路径;提供应用数据目录统一入口
|
||||
/// 和一次性迁移(从 getApplicationDocumentsDirectory() 迁移到
|
||||
/// getApplicationSupportDirectory())
|
||||
/// 上次更新: 新增 getAppDataDirectory() 和 migrateAppDataFromDocumentsToSupport(),
|
||||
/// 统一应用数据存储位置,避免桌面非沙盒下污染用户 ~/Documents 目录。
|
||||
/// 保留 getAppDirPathImpl() 用于用户可见文件(导出/分享)。
|
||||
/// ============================================================
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:xianyan/core/utils/logger.dart';
|
||||
|
||||
/// 旧的应用数据存储位置(仅用于用户可见文件:导出、分享等)
|
||||
///
|
||||
/// ⚠️ 不推荐用于应用内部数据存储。应用内部数据请使用 [getAppDataDirectory]。
|
||||
/// 原因:在 macOS/Windows/Linux 非沙盒模式下,此路径为用户公共 ~/Documents,
|
||||
/// 污染用户文档目录且存在被误删风险。
|
||||
Future<String?> getAppDirPathImpl() async {
|
||||
try {
|
||||
return (await getApplicationDocumentsDirectory()).path;
|
||||
@@ -16,6 +29,23 @@ Future<String?> getAppDirPathImpl() async {
|
||||
}
|
||||
}
|
||||
|
||||
/// 新的应用数据存储位置(应用专属,推荐用于应用内部数据)
|
||||
///
|
||||
/// 各平台路径:
|
||||
/// - macOS 非沙盒:`~/Library/Application Support/apps.xy.xianyan`
|
||||
/// - macOS 沙盒:`~/Library/Containers/apps.xy.xianyan/Data/Library/Application Support`
|
||||
/// - iOS:`<app>/Library/Application Support`
|
||||
/// - Windows:`%APPDATA%/<vendor>/<app>`
|
||||
/// - Linux:`~/.local/share/<app>`
|
||||
/// - Android:`/data/data/<pkg>/files`(与 Documents 相同)
|
||||
Future<String?> getAppDataDirectory() async {
|
||||
try {
|
||||
return (await getApplicationSupportDirectory()).path;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getTempDirPathImpl() async {
|
||||
try {
|
||||
return (await getTemporaryDirectory()).path;
|
||||
@@ -23,3 +53,150 @@ Future<String?> getTempDirPathImpl() async {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 一次性迁移:Documents → Application Support
|
||||
// ============================================================
|
||||
|
||||
/// 已知的应用数据子目录名(用于一次性迁移)
|
||||
const _kAppDataSubdirs = <String>[
|
||||
'image_cache', // 图片缓存
|
||||
'readlater_sync', // 稍后读同步
|
||||
'chat_audio', // 聊天录音
|
||||
'chat_attachments', // 聊天附件
|
||||
'chat_trash', // 聊天回收站
|
||||
'cloud_cache', // 云端缓存
|
||||
'file_transfer', // 文件传输
|
||||
'fonts', // 字体文件
|
||||
'xianyan_backups', // 自动备份(与 backup_service.dart 的 _backupDirName 保持一致)
|
||||
];
|
||||
|
||||
/// Hive 文件扩展名(用于迁移 Hive box 文件)
|
||||
const _kHiveFileExtensions = ['.hive', '.lock'];
|
||||
|
||||
/// 标记迁移是否已完成(避免重复迁移,进程内缓存)
|
||||
bool _migrationCompleted = false;
|
||||
|
||||
/// 将应用数据从 getApplicationDocumentsDirectory() 迁移到
|
||||
/// getApplicationSupportDirectory()
|
||||
///
|
||||
/// **迁移策略**:
|
||||
/// 1. 若两个路径相同(Android),跳过迁移
|
||||
/// 2. 复制旧路径下的应用数据子目录到新路径
|
||||
/// 3. 复制旧路径下的 Hive 文件(*.hive, *.lock)到新路径
|
||||
/// 4. 复制成功后删除旧路径下的对应文件/目录
|
||||
///
|
||||
/// **安全性**:
|
||||
/// - 使用 copy 而非 rename,复制成功后才删除旧文件
|
||||
/// - 若新路径已存在同名文件/目录,跳过(避免覆盖)
|
||||
/// - 单个文件/目录迁移失败不影响其他迁移
|
||||
/// - 详细的日志记录
|
||||
///
|
||||
/// 此函数应在应用启动时、Hive 初始化之前调用一次。
|
||||
Future<void> migrateAppDataFromDocumentsToSupport() async {
|
||||
if (_migrationCompleted) return;
|
||||
|
||||
try {
|
||||
final oldDir = await getApplicationDocumentsDirectory();
|
||||
final newDir = await getApplicationSupportDirectory();
|
||||
|
||||
// Android 上两个路径相同,无需迁移
|
||||
if (oldDir.path == newDir.path) {
|
||||
_migrationCompleted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await oldDir.exists()) {
|
||||
_migrationCompleted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保新目录存在
|
||||
await newDir.create(recursive: true);
|
||||
|
||||
var migratedCount = 0;
|
||||
|
||||
// 1. 迁移已知应用数据子目录
|
||||
for (final subdirName in _kAppDataSubdirs) {
|
||||
try {
|
||||
final oldSubdir = Directory(p.join(oldDir.path, subdirName));
|
||||
final newSubdir = Directory(p.join(newDir.path, subdirName));
|
||||
|
||||
if (!await oldSubdir.exists()) continue;
|
||||
// 若新路径已存在同名目录,跳过(避免覆盖)
|
||||
if (await newSubdir.exists()) continue;
|
||||
|
||||
// 复制整个子目录
|
||||
await _copyDirectory(oldSubdir, newSubdir);
|
||||
Log.i('AppDataMigration: 已迁移目录 $subdirName');
|
||||
migratedCount++;
|
||||
|
||||
// 复制成功后删除旧目录
|
||||
try {
|
||||
await oldSubdir.delete(recursive: true);
|
||||
} catch (e) {
|
||||
Log.w('AppDataMigration: 删除旧目录 $subdirName 失败', e);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('AppDataMigration: 迁移目录 $subdirName 失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 迁移 Hive 文件(*.hive, *.lock)
|
||||
try {
|
||||
await for (final entity in oldDir.list()) {
|
||||
if (entity is! File) continue;
|
||||
final fileName = p.basename(entity.path);
|
||||
final isHiveFile = _kHiveFileExtensions.any(
|
||||
(ext) => fileName.endsWith(ext),
|
||||
);
|
||||
if (!isHiveFile) continue;
|
||||
|
||||
try {
|
||||
final newFile = File(p.join(newDir.path, fileName));
|
||||
if (await newFile.exists()) continue; // 已存在,跳过
|
||||
|
||||
await entity.copy(newFile.path);
|
||||
Log.i('AppDataMigration: 已迁移 Hive 文件 $fileName');
|
||||
migratedCount++;
|
||||
|
||||
// 复制成功后删除旧文件
|
||||
try {
|
||||
await entity.delete();
|
||||
} catch (e) {
|
||||
Log.w('AppDataMigration: 删除旧 Hive 文件 $fileName 失败', e);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('AppDataMigration: 迁移 Hive 文件 $fileName 失败', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('AppDataMigration: 扫描 Hive 文件失败', e);
|
||||
}
|
||||
|
||||
if (migratedCount > 0) {
|
||||
Log.i('AppDataMigration: 迁移完成,共迁移 $migratedCount 个项目');
|
||||
} else {
|
||||
Log.i('AppDataMigration: 无需迁移(未发现旧路径应用数据)');
|
||||
}
|
||||
_migrationCompleted = true;
|
||||
} catch (e) {
|
||||
Log.e('AppDataMigration: 迁移失败', e);
|
||||
// 不标记完成,下次启动可重试
|
||||
}
|
||||
}
|
||||
|
||||
/// 递归复制目录
|
||||
Future<void> _copyDirectory(Directory source, Directory destination) async {
|
||||
await destination.create(recursive: true);
|
||||
await for (final entity in source.list()) {
|
||||
if (entity is File) {
|
||||
await entity.copy(p.join(destination.path, p.basename(entity.path)));
|
||||
} else if (entity is Directory) {
|
||||
await _copyDirectory(
|
||||
entity,
|
||||
Directory(p.join(destination.path, p.basename(entity.path))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — path_provider Web Stub
|
||||
/// 创建时间: 2026-05-20
|
||||
/// 更新时间: 2026-05-20
|
||||
/// 更新时间: 2026-06-26
|
||||
/// 作用: Web端path_provider不可用,提供空实现返回null
|
||||
/// 上次更新: 初始创建
|
||||
/// 上次更新: 同步新增 getAppDataDirectory() 和 migrateAppDataFromDocumentsToSupport()
|
||||
/// 的 Web 端空实现
|
||||
/// ============================================================
|
||||
|
||||
Future<String?> getAppDirPathImpl() async => null;
|
||||
|
||||
Future<String?> getAppDataDirectory() async => null;
|
||||
|
||||
Future<String?> getTempDirPathImpl() async => null;
|
||||
|
||||
/// Web 端无文件系统,迁移为空操作
|
||||
Future<void> migrateAppDataFromDocumentsToSupport() async {}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 平台工具类
|
||||
/// 创建时间: 2026-04-25
|
||||
/// 更新时间: 2026-06-25
|
||||
/// 更新时间: 2026-06-26
|
||||
/// 作用: 封装平台相关操作,隔离dart:io/dart:html,支持鸿蒙
|
||||
/// 上次更新: 新增isIntelMac检测Intel CPU MacBook
|
||||
/// 上次更新: 新增 safeAppDataPath 和 migrateAppData,统一应用数据存储位置
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
@@ -30,11 +30,25 @@ bool get supportsWebView3D => supportsWebView3DImpl;
|
||||
bool get isIntelMac => isIntelMacImpl;
|
||||
|
||||
/// 安全获取应用文档目录路径(Web端返回null)
|
||||
///
|
||||
/// ⚠️ 仅用于用户可见文件(导出、分享等)。应用内部数据请使用 [safeAppDataPath]。
|
||||
Future<String?> get safeAppDirPath => getAppDirPathImpl();
|
||||
|
||||
/// 安全获取应用数据目录路径(Web端返回null)
|
||||
///
|
||||
/// 推荐用于应用内部数据存储(数据库、缓存、Hive、附件等)。
|
||||
/// 返回应用专属目录,避免污染用户 ~/Documents。
|
||||
Future<String?> get safeAppDataPath => getAppDataDirectory();
|
||||
|
||||
/// 安全获取临时目录路径(Web端返回null)
|
||||
Future<String?> get safeTempDirPath => getTempDirPathImpl();
|
||||
|
||||
/// 将应用数据从 Documents 目录迁移到 Application Support 目录
|
||||
///
|
||||
/// 应在应用启动时、Hive 初始化之前调用一次。
|
||||
/// 详见 path_provider_native.dart 中的实现说明。
|
||||
Future<void> migrateAppData() => migrateAppDataFromDocumentsToSupport();
|
||||
|
||||
/// 鸿蒙端设备特性检测
|
||||
class OhosDeviceCapabilities {
|
||||
OhosDeviceCapabilities._();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 工具库统一导出
|
||||
/// 创建时间: 2026-05-23
|
||||
/// 更新时间: 2026-05-31
|
||||
/// 更新时间: 2026-06-26
|
||||
/// 作用: barrel export,统一导出所有 utils 公共 API
|
||||
/// 上次更新: 新增rtl_utils导出
|
||||
/// 上次更新: 同步 path_provider 新增函数的 hide 列表
|
||||
/// ============================================================
|
||||
|
||||
// ── Root ──────────────────────────────────────────────────────
|
||||
@@ -18,8 +18,8 @@ export 'platform/clipboard_bridge.dart';
|
||||
export 'platform/isolate_stub.dart' hide Isolate, RawReceivePort, SendPort;
|
||||
export 'platform/platform_io_stub.dart' hide isWebImpl, isOhosImpl, isAndroidImpl, isIOSImpl, isMacOSImpl, isWindowsImpl, isLinuxImpl, isMobileImpl, isDesktopImpl, platformNameImpl, platformVersionImpl, supportsFilesystemImpl, supportsGPU3DImpl, supportsWebView3DImpl, isIntelMacImpl;
|
||||
export 'platform/platform_io_native.dart' hide isWebImpl, isOhosImpl, isAndroidImpl, isIOSImpl, isMacOSImpl, isWindowsImpl, isLinuxImpl, isMobileImpl, isDesktopImpl, platformNameImpl, platformVersionImpl, supportsFilesystemImpl, supportsGPU3DImpl, supportsWebView3DImpl, isIntelMacImpl;
|
||||
export 'platform/path_provider_stub.dart' hide getAppDirPathImpl, getTempDirPathImpl;
|
||||
export 'platform/path_provider_native.dart' hide getAppDirPathImpl, getTempDirPathImpl;
|
||||
export 'platform/path_provider_stub.dart' hide getAppDirPathImpl, getAppDataDirectory, getTempDirPathImpl, migrateAppDataFromDocumentsToSupport;
|
||||
export 'platform/path_provider_native.dart' hide getAppDirPathImpl, getAppDataDirectory, getTempDirPathImpl, migrateAppDataFromDocumentsToSupport;
|
||||
|
||||
// ── UI ────────────────────────────────────────────────────────
|
||||
export 'ui/interaction_animations.dart';
|
||||
|
||||
@@ -52,7 +52,7 @@ class ChatAudioService {
|
||||
return null;
|
||||
}
|
||||
if (kIsWeb) throw UnsupportedError('Web端不支持文件系统');
|
||||
final appDocDir = await getApplicationDocumentsDirectory();
|
||||
final appDocDir = await getApplicationSupportDirectory();
|
||||
final audioDir = Directory('${appDocDir.path}/chat_audio');
|
||||
if (!await audioDir.exists()) {
|
||||
await audioDir.create(recursive: true);
|
||||
|
||||
@@ -24,7 +24,7 @@ class ChatFileService {
|
||||
|
||||
/// 获取聊天附件根目录
|
||||
static Future<Directory> getChatRootDir() async {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final appDir = await getApplicationSupportDirectory();
|
||||
final chatDir = Directory('${appDir.path}/chat_attachments');
|
||||
if (!chatDir.existsSync()) {
|
||||
chatDir.createSync(recursive: true);
|
||||
@@ -113,6 +113,9 @@ class ChatFileService {
|
||||
|
||||
/// 获取文件的绝对路径
|
||||
/// 如果传入的已经是绝对路径或网络地址,直接返回
|
||||
///
|
||||
/// 兼容新旧两个路径:优先使用 Application Support(新路径),
|
||||
/// 若文件不存在则回退到 Documents(旧路径,兼容迁移前残留文件)
|
||||
static Future<String> getAbsolutePath(String relativePath) async {
|
||||
if (relativePath.startsWith('http') || relativePath.startsWith('/')) {
|
||||
return relativePath;
|
||||
@@ -121,8 +124,28 @@ class ChatFileService {
|
||||
return relativePath;
|
||||
}
|
||||
if (kIsWeb) throw UnsupportedError('Web端不支持文件系统');
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
return '${appDir.path}/$relativePath';
|
||||
|
||||
// 优先使用新路径:Application Support
|
||||
try {
|
||||
final supportDir = await getApplicationSupportDirectory();
|
||||
final newPath = '${supportDir.path}/$relativePath';
|
||||
if (await File(newPath).exists()) {
|
||||
return newPath;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 回退到旧路径:Documents(兼容迁移前残留文件)
|
||||
try {
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
final oldPath = '${docDir.path}/$relativePath';
|
||||
if (await File(oldPath).exists()) {
|
||||
return oldPath;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 都不存在时返回新路径(让调用方处理不存在的情况)
|
||||
final supportDir = await getApplicationSupportDirectory();
|
||||
return '${supportDir.path}/$relativePath';
|
||||
}
|
||||
|
||||
/// 判断是否为沙箱相对路径(以chat_attachments/开头)
|
||||
|
||||
@@ -58,7 +58,7 @@ class CacheManagerService {
|
||||
];
|
||||
|
||||
Future<String> _getBasePath() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
return '${dir.path}${Platform.pathSeparator}file_transfer';
|
||||
}
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ class CloudCacheService {
|
||||
if (savePath != null) {
|
||||
outputPath = savePath;
|
||||
} else {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
outputPath =
|
||||
'${dir.path}${Platform.pathSeparator}cloud_cache${Platform.pathSeparator}${record.fileName}';
|
||||
await Directory(outputPath).parent.create(recursive: true);
|
||||
|
||||
@@ -178,7 +178,7 @@ class FontManagementNotifier extends Notifier<FontManagementState>
|
||||
Future<List<FontInfo>> _scanLocalFonts() async {
|
||||
if (pu.isWeb) return [];
|
||||
try {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final fontDir = Directory('${dir.path}/fonts');
|
||||
final List<FontInfo> fonts = [];
|
||||
|
||||
|
||||
@@ -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,187 @@ 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 关闭,文件可安全删除
|
||||
// 同时清理新旧两个路径:新路径为 getApplicationSupportDirectory(),
|
||||
// 旧路径为 getApplicationDocumentsDirectory()(兼容历史版本残留文件)
|
||||
final dbSearchDirs = <Directory>[];
|
||||
try {
|
||||
dbSearchDirs.add(await getApplicationSupportDirectory());
|
||||
} catch (_) {}
|
||||
try {
|
||||
dbSearchDirs.add(await getApplicationDocumentsDirectory());
|
||||
} catch (_) {}
|
||||
|
||||
for (final dir in dbSearchDirs) {
|
||||
try {
|
||||
final dirPath = dir.path;
|
||||
// 仅删除明确的应用数据库文件,绝不遍历目录
|
||||
for (final suffix in ['', '-wal', '-shm', '-journal']) {
|
||||
final dbFile = File(p.join(dirPath, '$_appDbName$suffix'));
|
||||
if (await dbFile.exists()) {
|
||||
try {
|
||||
await dbFile.delete();
|
||||
Log.i('FactoryReset: 已删除 ${dir.path}/$_appDbName$suffix');
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 删除 $_appDbName$suffix 失败', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('FactoryReset: 数据库文件清理失败 (${dir.path})', 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 清理 Documents 目录下已知应用子目录(不删除根内容)----
|
||||
// 兼容迁移失败时残留的应用数据子目录
|
||||
// 永不递归删除 getApplicationDocumentsDirectory() 返回目录的根内容
|
||||
try {
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
const appSubdirs = [
|
||||
'image_cache',
|
||||
'readlater_sync',
|
||||
'chat_audio',
|
||||
'chat_attachments',
|
||||
'chat_trash',
|
||||
'cloud_cache',
|
||||
'file_transfer',
|
||||
'fonts',
|
||||
'xianyan_backups',
|
||||
];
|
||||
for (final subdirName in appSubdirs) {
|
||||
try {
|
||||
final subdir = Directory(p.join(docDir.path, subdirName));
|
||||
if (await subdir.exists()) {
|
||||
await subdir.delete(recursive: true);
|
||||
Log.i('FactoryReset: 已删除 Documents/$subdirName');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// ---- 6.6 清理 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,37 +974,93 @@ class GeneralSettingsNotifier extends Notifier<GeneralSettingsState> {
|
||||
Future<void> clearCache() async {
|
||||
try {
|
||||
if (kIsWeb) return;
|
||||
final dirs = <String>[];
|
||||
|
||||
// 安全清理:避免递归删除 getApplicationDocumentsDirectory() 返回目录的根内容
|
||||
// 历史 bug:在 macOS/Windows/Linux 非沙盒下,该目录为用户公共 ~/Documents,
|
||||
// 递归删除会丢失用户项目源代码和其他文档
|
||||
const appBundleId = 'apps.xy.xianyan';
|
||||
|
||||
// 1. 清空临时目录(应用专属,安全)
|
||||
try {
|
||||
dirs.add((await getTemporaryDirectory()).path);
|
||||
} catch (_) {}
|
||||
try {
|
||||
dirs.add((await getApplicationDocumentsDirectory()).path);
|
||||
} catch (_) {}
|
||||
try {
|
||||
dirs.add((await getApplicationSupportDirectory()).path);
|
||||
final tmpDir = await getTemporaryDirectory();
|
||||
await _safeClearDirContents(tmpDir, '临时目录', requireAppId: false);
|
||||
} catch (_) {}
|
||||
|
||||
for (final path in dirs) {
|
||||
final dir = Directory(path);
|
||||
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 (_) {}
|
||||
}
|
||||
// 2. 清空应用支持目录(应用专属,带路径校验)
|
||||
try {
|
||||
final supportDir = await getApplicationSupportDirectory();
|
||||
await _safeClearDirContents(supportDir, '应用支持目录',
|
||||
requireAppId: true, appId: appBundleId);
|
||||
} catch (_) {}
|
||||
|
||||
// 3. 清理 Documents 目录下已知应用子目录(不删除根内容)
|
||||
try {
|
||||
final docDir = await getApplicationDocumentsDirectory();
|
||||
const appSubdirs = [
|
||||
'image_cache',
|
||||
'readlater_sync',
|
||||
'chat_audio',
|
||||
'chat_attachments',
|
||||
'chat_trash',
|
||||
'cloud_cache',
|
||||
'file_transfer',
|
||||
'fonts',
|
||||
'xianyan_backups',
|
||||
];
|
||||
for (final subdirName in appSubdirs) {
|
||||
try {
|
||||
final subdir = Directory('${docDir.path}/$subdirName');
|
||||
if (await subdir.exists()) {
|
||||
await subdir.delete(recursive: true);
|
||||
Log.i('clearCache: 已删除 Documents/$subdirName');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
state = state.copyWith(general: state.general.copyWith(cacheSize: 0));
|
||||
Log.i('缓存已清除');
|
||||
} catch (e) {
|
||||
Log.e('缓存清除失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全清空目录内容(不删除目录本身)
|
||||
///
|
||||
/// [requireAppId] 为 true 时,校验目录路径是否包含应用标识,避免误删用户公共目录
|
||||
Future<void> _safeClearDirContents(
|
||||
Directory dir,
|
||||
String label, {
|
||||
required bool requireAppId,
|
||||
String appId = '',
|
||||
}) async {
|
||||
try {
|
||||
if (!await dir.exists()) return;
|
||||
|
||||
if (requireAppId) {
|
||||
final dirPath = dir.path.toLowerCase();
|
||||
final isAppSpecific = dirPath.contains(appId.toLowerCase()) ||
|
||||
dirPath.contains('xianyan');
|
||||
if (!isAppSpecific) {
|
||||
Log.w('clearCache: $label 路径未包含应用标识,跳过清理: ${dir.path}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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 (_) {}
|
||||
}
|
||||
Log.i('clearCache: $label 已清空');
|
||||
} catch (e) {
|
||||
Log.w('clearCache: 清理 $label 失败', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final generalSettingsProvider =
|
||||
|
||||
@@ -67,9 +67,9 @@ class FontDownloadService {
|
||||
if (pu.isWeb) throw UnsupportedError('Web端不支持字体管理');
|
||||
Directory dir;
|
||||
try {
|
||||
dir = await getApplicationDocumentsDirectory();
|
||||
dir = await getApplicationSupportDirectory();
|
||||
} catch (e) {
|
||||
Log.w('获取应用文档目录失败,降级使用临时目录', e);
|
||||
Log.w('获取应用支持目录失败,降级使用临时目录', e);
|
||||
dir = Directory.systemTemp;
|
||||
}
|
||||
final fontDir = Directory('${dir.path}/fonts');
|
||||
|
||||
@@ -164,6 +164,15 @@ Future<void> _appMain() async {
|
||||
Log.e('AppVersion 初始化失败', e, st, LogCategory.general);
|
||||
}
|
||||
|
||||
// 应用数据目录迁移:Documents → Application Support
|
||||
// 必须在 KvStorage/Hive 初始化之前执行,确保存储初始化时使用新路径
|
||||
// 详见 path_provider_native.dart 中的 migrateAppDataFromDocumentsToSupport()
|
||||
try {
|
||||
await pu.migrateAppData();
|
||||
} catch (e, st) {
|
||||
Log.e('应用数据迁移失败', e, st, LogCategory.storage);
|
||||
}
|
||||
|
||||
try {
|
||||
await KvStorage.init();
|
||||
kvStorageReady = true;
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- 启用沙盒:让 Debug 模式与 Release 模式路径行为一致,避免开发环境差异
|
||||
导致的 bug(如 getApplicationDocumentsDirectory() 在非沙盒下返回用户公共
|
||||
~/Documents 目录,曾导致 FactoryReset 误删项目源代码)。
|
||||
代价:Debug 模式下旧版数据库(位于 ~/Documents/xianyan.db 或
|
||||
~/Library/Application Support/apps.xy.xianyan/xianyan.db)无法迁移到
|
||||
沙盒内,开发者需重新登录/创建数据。 -->
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
@@ -21,7 +27,7 @@
|
||||
<!-- 麦克风:语音录制 / 转文字(AVCaptureDevice via PermissionManager.swift) -->
|
||||
<key>com.apple.security.device.microphone</key>
|
||||
<true/>
|
||||
<!-- 定位:permission_handler 依赖链引用 CoreLocation 符号,沙盒需此 entitlement -->
|
||||
<!-- 定位:permission_handler 依赖链引用 CoreLocation,沙盒需此 entitlement -->
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
Reference in New Issue
Block a user