Files
ChatLab/electron/main/utils/pathUtils.ts
T

228 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as fs from 'fs'
import * as path from 'path'
// 系统关键目录列表(用于安全校验)
const DANGEROUS_PATHS = [
// Windows 系统目录
'C:\\Windows',
'C:\\Program Files',
'C:\\Program Files (x86)',
'C:\\ProgramData',
// Unix 系统目录
'/usr',
'/etc',
'/bin',
'/sbin',
'/lib',
'/var',
'/boot',
'/root',
'/System',
'/Library',
]
// 统一路径标准化(兼容 Windows 大小写差异)
function normalizePathForCompare(input: string): string {
const resolved = path.resolve(input)
const normalized = path.normalize(resolved)
return process.platform === 'win32' ? normalized.toLowerCase() : normalized
}
/**
* 判断 child 是否为 parent 的子目录
*/
export function isSubPath(parent: string, child: string): boolean {
const parentPath = normalizePathForCompare(parent)
const childPath = normalizePathForCompare(child)
if (parentPath === childPath) return false
return childPath.startsWith(`${parentPath}${path.sep}`)
}
/**
* 检查路径是否安全(不在系统关键目录下)
*/
export function isPathSafe(targetPath: string): boolean {
const normalizedTarget = targetPath.toLowerCase().replace(/\//g, '\\')
for (const dangerous of DANGEROUS_PATHS) {
const normalizedDangerous = dangerous.toLowerCase().replace(/\//g, '\\')
if (normalizedTarget.startsWith(normalizedDangerous)) {
return false
}
}
return true
}
/**
* 检查目录是否为空或包含 ChatLab 标记与关键结构
*/
export function isDirectorySafeToUse(dirPath: string, markerFile: string, requiredDirs: string[]): boolean {
if (!fs.existsSync(dirPath)) {
return true // 目录不存在,可以安全使用
}
try {
const entries = fs.readdirSync(dirPath)
// 如果目录为空,可以安全使用
if (entries.length === 0) return true
return hasChatLabStructure(entries, markerFile, requiredDirs)
} catch {
return false
}
}
/**
* 检查目录是否为已存在的 ChatLab 数据目录
*/
export function isExistingChatLabDir(dirPath: string, markerFile: string, requiredDirs: string[]): boolean {
if (!fs.existsSync(dirPath)) return false
try {
const entries = fs.readdirSync(dirPath)
return hasChatLabStructure(entries, markerFile, requiredDirs)
} catch {
return false
}
}
/**
* 确保数据目录标记文件存在
*/
export function ensureMarkerFile(dirPath: string, markerFile: string): void {
try {
const markerPath = path.join(dirPath, markerFile)
if (!fs.existsSync(markerPath)) {
fs.writeFileSync(markerPath, 'ChatLab Data Directory', 'utf-8')
}
} catch {
// 标记文件写入失败时静默处理
}
}
/**
* 递归复制目录
*/
export function copyDirRecursive(src: string, dest: string, ensureDir: (dirPath: string) => void): void {
ensureDir(dest)
const entries = fs.readdirSync(src, { withFileTypes: true })
for (const entry of entries) {
const srcPath = path.join(src, entry.name)
const destPath = path.join(dest, entry.name)
if (entry.isDirectory()) {
copyDirRecursive(srcPath, destPath, ensureDir)
} else {
fs.copyFileSync(srcPath, destPath)
}
}
}
export interface CopyStats {
copied: number
skipped: number
errors: string[]
}
/**
* 递归合并复制目录(仅复制目标不存在的文件)
* @returns 复制结果统计
*/
export function copyDirMerge(
src: string,
dest: string,
ensureDir: (dirPath: string) => void,
stats: CopyStats = { copied: 0, skipped: 0, errors: [] }
): CopyStats {
if (!fs.existsSync(src)) return stats
try {
ensureDir(dest)
const entries = fs.readdirSync(src, { withFileTypes: true })
for (const entry of entries) {
const srcPath = path.join(src, entry.name)
const destPath = path.join(dest, entry.name)
try {
if (entry.isDirectory()) {
if (!fs.existsSync(destPath)) {
copyDirRecursive(srcPath, destPath, ensureDir)
stats.copied++
} else {
copyDirMerge(srcPath, destPath, ensureDir, stats)
}
} else {
if (!fs.existsSync(destPath)) {
fs.copyFileSync(srcPath, destPath)
stats.copied++
} else {
stats.skipped++
}
}
} catch (error) {
const errorMsg = `复制失败: ${srcPath} -> ${error instanceof Error ? error.message : String(error)}`
console.error('[Paths]', errorMsg)
stats.errors.push(errorMsg)
}
}
} catch (error) {
const errorMsg = `读取目录失败: ${src} -> ${error instanceof Error ? error.message : String(error)}`
console.error('[Paths]', errorMsg)
stats.errors.push(errorMsg)
}
return stats
}
/**
* 写入迁移日志到 app.log
*/
export function writeMigrationLog(logDir: string, message: string, ensureDir: (dirPath: string) => void): void {
try {
ensureDir(logDir)
const logPath = path.join(logDir, 'app.log')
const now = new Date()
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
const logLine = `[${timestamp}] [MIGRATION] ${message}\n`
fs.appendFileSync(logPath, logLine, 'utf-8')
} catch {
// 日志写入失败时静默处理
}
}
function hasChatLabStructure(entries: string[], markerFile: string, requiredDirs: string[]): boolean {
const hasMarker = entries.includes(markerFile)
const hasRequiredDirs = requiredDirs.every((dir) => entries.includes(dir))
return hasMarker && hasRequiredDirs
}
/**
* 获取应用安装根目录
* macOS: .app 包路径(如 /Applications/ChatLab.app
* Windows/Linux: 可执行文件所在目录
*/
export function getAppInstallDir(exePath: string): string {
if (process.platform === 'darwin') {
const appBundleMatch = exePath.match(/^(.+?\.app)(\/|$)/)
if (appBundleMatch) {
return appBundleMatch[1]
}
}
return path.dirname(exePath)
}
/**
* 检查目标路径是否位于应用安装目录内(或等于安装目录)
*/
export function isInsideAppInstallDir(targetPath: string, exePath: string): boolean {
const installDir = getAppInstallDir(exePath)
const normalizedTarget = normalizePathForCompare(targetPath)
const normalizedInstall = normalizePathForCompare(installDir)
return normalizedTarget === normalizedInstall || normalizedTarget.startsWith(`${normalizedInstall}${path.sep}`)
}