Files
ChatLab/electron/main/utils/pathUtils.ts
2026-02-03 00:25:09 +08:00

206 lines
5.6 KiB
TypeScript

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
}