mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-27 15:29:49 +08:00
206 lines
5.6 KiB
TypeScript
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
|
|
}
|