Code audit fixes: - CRITICAL: reorder Markdown pipeline (fixImages before sanitize) - CRITICAL: fix path prefix separator check - BLOCKING: remove duplicate useEffect in Editor - BLOCKING: skip onChange when content unchanged - BLOCKING: optimize Sidebar re-render with useMemo - HIGH: cleanup FileWatcher polling intervals - HIGH: improve validatePath segment check - HIGH: fix isExternalUpdate race with counter - HIGH: add will-navigate / setWindowOpenHandler - HIGH: explicit strip in sanitize schema - MEDIUM: random temp file suffix instead of Date.now() - MEDIUM/LOW: add IndexedDB error boundaries - LOW: support UTF-16 BOM detection
130 lines
3.4 KiB
TypeScript
130 lines
3.4 KiB
TypeScript
import * as fs from 'fs'
|
|
import { BrowserWindow } from 'electron'
|
|
|
|
export class FileWatcher {
|
|
private watcher: fs.FSWatcher | null = null
|
|
private currentPath: string | null = null
|
|
private isSelfWriting = false
|
|
private pollTimer: ReturnType<typeof setInterval> | null = null
|
|
private pollTimeout: ReturnType<typeof setTimeout> | null = null
|
|
|
|
constructor(private getMainWindow: () => BrowserWindow | null) {}
|
|
|
|
start(filePath: string): void {
|
|
this.stop()
|
|
if (!filePath) return
|
|
try {
|
|
this.currentPath = filePath
|
|
this.watcher = fs.watch(filePath, (eventType) => {
|
|
if (eventType === 'change') {
|
|
if (this.isSelfWriting) return
|
|
const win = this.getMainWindow()
|
|
if (win && !win.isDestroyed()) {
|
|
win.webContents.send('file:externallyModified', filePath)
|
|
}
|
|
}
|
|
})
|
|
// L-02: 监听 error 事件,文件被删除时显式关闭并清理状态
|
|
this.watcher.on('error', () => {
|
|
const originalPath = this.currentPath
|
|
this.stop()
|
|
if (originalPath) {
|
|
this.pollTimer = setInterval(() => {
|
|
try {
|
|
if (fs.existsSync(originalPath)) {
|
|
this.clearPollTimers()
|
|
this.start(originalPath)
|
|
}
|
|
} catch {
|
|
this.clearPollTimers()
|
|
}
|
|
}, 2000)
|
|
// 最多轮询30秒
|
|
this.pollTimeout = setTimeout(() => this.clearPollTimers(), 30000)
|
|
}
|
|
})
|
|
} catch {
|
|
// silently ignore
|
|
}
|
|
}
|
|
|
|
stop(): void {
|
|
this.clearPollTimers()
|
|
if (this.watcher) {
|
|
this.watcher.close()
|
|
this.watcher = null
|
|
}
|
|
this.currentPath = null
|
|
}
|
|
|
|
private clearPollTimers(): void {
|
|
if (this.pollTimer) {
|
|
clearInterval(this.pollTimer)
|
|
this.pollTimer = null
|
|
}
|
|
if (this.pollTimeout) {
|
|
clearTimeout(this.pollTimeout)
|
|
this.pollTimeout = null
|
|
}
|
|
}
|
|
|
|
getCurrentPath(): string | null {
|
|
return this.currentPath
|
|
}
|
|
|
|
setSelfWriting(value: boolean): void {
|
|
this.isSelfWriting = value
|
|
}
|
|
}
|
|
|
|
export class SidebarWatcher {
|
|
private watcher: fs.FSWatcher | null = null
|
|
private watchPath: string | null = null
|
|
private refreshTimer: NodeJS.Timeout | null = null
|
|
|
|
constructor(private getMainWindow: () => BrowserWindow | null) {}
|
|
|
|
start(dirPath: string): void {
|
|
this.stop()
|
|
if (!dirPath) return
|
|
try {
|
|
this.watchPath = dirPath
|
|
this.watcher = fs.watch(dirPath, { recursive: true }, () => {
|
|
const win = this.getMainWindow()
|
|
if (win && !win.isDestroyed()) {
|
|
if (this.refreshTimer) clearTimeout(this.refreshTimer)
|
|
this.refreshTimer = setTimeout(() => {
|
|
// M-03: 延迟后再次检查窗口状态
|
|
const w = this.getMainWindow()
|
|
if (w && !w.isDestroyed()) {
|
|
w.webContents.send('sidebar:dirChanged')
|
|
}
|
|
}, 300)
|
|
}
|
|
})
|
|
// L-02: 监听 error 事件,显式关闭
|
|
this.watcher.on('error', () => {
|
|
if (this.watcher) {
|
|
this.watcher.close()
|
|
this.watcher = null
|
|
}
|
|
this.watchPath = null
|
|
})
|
|
} catch {
|
|
// silently ignore
|
|
}
|
|
}
|
|
|
|
stop(): void {
|
|
if (this.watcher) {
|
|
this.watcher.close()
|
|
this.watcher = null
|
|
}
|
|
if (this.refreshTimer) {
|
|
clearTimeout(this.refreshTimer)
|
|
this.refreshTimer = null
|
|
}
|
|
this.watchPath = null
|
|
}
|
|
}
|