mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
/**
|
|
* Desktop data-directory switch lifecycle.
|
|
*/
|
|
|
|
import { app } from 'electron'
|
|
import * as fs from 'node:fs'
|
|
import * as path from 'node:path'
|
|
import { writeConfigField } from '@openchatlab/config'
|
|
import {
|
|
createPendingDataDirMigration,
|
|
isDirectoryEmptyOrMissing,
|
|
isExistingUserDataDir,
|
|
isUserDataDirSafeToUse,
|
|
registerPendingDataDirCleanup,
|
|
runPendingDataDirMigration,
|
|
} from '@openchatlab/node-runtime/data-dir-switch'
|
|
import { isInsideAppInstallDir, isPathSafe, isSubPath, writeMigrationLog } from '../utils/pathUtils'
|
|
import {
|
|
ensureDir,
|
|
getDefaultUserDataDir,
|
|
getLogsDir,
|
|
getSystemDataDir,
|
|
getUserDataDir,
|
|
setCachedUserDataDir,
|
|
} from './locations'
|
|
import { readStorageConfig, writeStorageConfig } from './storage-config'
|
|
|
|
/**
|
|
* 设置用户数据目录
|
|
* @param dataDir 目标目录(为空则恢复默认)
|
|
* @param migrate 是否迁移现有数据(合并复制,不会覆盖目标文件)
|
|
*/
|
|
export function setCustomDataDir(
|
|
dataDir: string | null,
|
|
migrate: boolean = true
|
|
): { success: boolean; error?: string; from?: string; to?: string; requiresRelaunch?: boolean } {
|
|
const normalized = typeof dataDir === 'string' ? dataDir.trim() : ''
|
|
const oldDir = getUserDataDir()
|
|
|
|
try {
|
|
if (process.env.CHATLAB_DATA_DIR) {
|
|
return { success: false, error: 'CHATLAB_DATA_DIR 已设置,不能在界面中切换数据目录' }
|
|
}
|
|
|
|
const newDir = normalized || getDefaultUserDataDir()
|
|
|
|
if (!path.isAbsolute(newDir)) {
|
|
return { success: false, error: '数据目录必须是绝对路径' }
|
|
}
|
|
|
|
if (migrate && oldDir !== newDir && isSubPath(oldDir, newDir)) {
|
|
return { success: false, error: '目标目录不能是当前数据目录的子目录' }
|
|
}
|
|
|
|
if (!isPathSafe(newDir)) {
|
|
return { success: false, error: '不能使用系统关键目录作为数据目录' }
|
|
}
|
|
|
|
try {
|
|
const exePath = app.getPath('exe')
|
|
if (isInsideAppInstallDir(newDir, exePath)) {
|
|
return { success: false, error: '不能将数据目录放在应用安装目录下,应用更新时该目录会被清空' }
|
|
}
|
|
} catch {
|
|
// 获取 exe 路径失败时跳过此检查
|
|
}
|
|
|
|
if (!isUserDataDirSafeToUse(newDir)) {
|
|
return { success: false, error: '目标目录不为空且不包含 ChatLab 数据,请选择空目录或已有数据目录' }
|
|
}
|
|
|
|
if (path.resolve(oldDir) === path.resolve(newDir)) {
|
|
const config = readStorageConfig()
|
|
writeStorageConfig({ ...config, pendingDataDirMigration: undefined })
|
|
return { success: true, from: oldDir, to: newDir, requiresRelaunch: false }
|
|
}
|
|
|
|
const config = readStorageConfig()
|
|
const pending = createPendingDataDirMigration({
|
|
from: oldDir,
|
|
to: newDir,
|
|
migrate,
|
|
targetWasEmpty: isDirectoryEmptyOrMissing(newDir),
|
|
})
|
|
writeStorageConfig({ ...config, pendingDataDirMigration: pending })
|
|
|
|
return { success: true, from: oldDir, to: newDir, requiresRelaunch: true }
|
|
} catch (error) {
|
|
console.error('[Paths] Error setting custom data dir:', error)
|
|
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
|
}
|
|
}
|
|
|
|
export function applyPendingDataDirMigration(): { success: boolean; skipped?: boolean; error?: string } {
|
|
const config = readStorageConfig()
|
|
const pending = config.pendingDataDirMigration
|
|
if (!pending) return { success: true, skipped: true }
|
|
|
|
const result = runPendingDataDirMigration(pending, {
|
|
writeUserDataDir(dir) {
|
|
writeConfigField('data', 'user_data_dir', dir)
|
|
writeConfigField('data', 'electron_migration_done', true)
|
|
setCachedUserDataDir(dir)
|
|
},
|
|
clearPendingMigration() {
|
|
const latest = readStorageConfig()
|
|
writeStorageConfig({ ...latest, pendingDataDirMigration: undefined })
|
|
},
|
|
recordPendingCleanup(sourceDir, targetDir) {
|
|
registerPendingDataDirCleanup(getSystemDataDir(), { sourceDir, targetDir })
|
|
},
|
|
log(message) {
|
|
writeMigrationLog(getLogsDir(), message, ensureDir)
|
|
},
|
|
})
|
|
|
|
if (!result.success) {
|
|
const error = result.errors.join('; ') || '数据目录迁移失败'
|
|
console.warn('[Paths] Pending data dir migration failed:', error)
|
|
writeMigrationLog(
|
|
getLogsDir(),
|
|
`切换目录迁移失败: 从 ${pending.from} 到 ${pending.to},复制 ${result.copied} 项,跳过 ${result.skipped} 项,错误 ${result.errors.length} 项: ${error}`,
|
|
ensureDir
|
|
)
|
|
return { success: false, error }
|
|
}
|
|
|
|
writeMigrationLog(
|
|
getLogsDir(),
|
|
`切换目录迁移成功: 从 ${pending.from} 到 ${pending.to},复制 ${result.copied} 项,跳过 ${result.skipped} 项`,
|
|
ensureDir
|
|
)
|
|
return { success: true }
|
|
}
|
|
|
|
/**
|
|
* 将旧版本遗留的自动删除任务转换为人工清理记录。
|
|
* 旧目录只保留为备份,绝不在应用启动期间自动删除。
|
|
*/
|
|
export function preserveLegacyPendingDeleteDir(): void {
|
|
try {
|
|
const config = readStorageConfig()
|
|
const pendingDir = config.pendingDeleteDir
|
|
|
|
if (!pendingDir) return
|
|
|
|
const currentDir = getUserDataDir()
|
|
|
|
if (path.resolve(pendingDir) === path.resolve(currentDir)) {
|
|
writeStorageConfig({ ...config, pendingDeleteDir: undefined })
|
|
return
|
|
}
|
|
|
|
if (!isPathSafe(pendingDir)) {
|
|
writeStorageConfig({ ...config, pendingDeleteDir: undefined })
|
|
return
|
|
}
|
|
|
|
if (fs.existsSync(pendingDir) && !isExistingUserDataDir(pendingDir)) {
|
|
writeStorageConfig({ ...config, pendingDeleteDir: undefined })
|
|
return
|
|
}
|
|
|
|
if (!fs.existsSync(pendingDir)) {
|
|
writeStorageConfig({ ...config, pendingDeleteDir: undefined })
|
|
return
|
|
}
|
|
|
|
registerPendingDataDirCleanup(getSystemDataDir(), { sourceDir: pendingDir, targetDir: currentDir })
|
|
writeMigrationLog(getLogsDir(), `Old data directory preserved for manual cleanup: ${pendingDir}`, ensureDir)
|
|
writeStorageConfig({ ...config, pendingDeleteDir: undefined })
|
|
} catch (error) {
|
|
console.error('[Paths] Failed to preserve legacy cleanup record:', error)
|
|
}
|
|
}
|