这是一个非常硬核的系统级需求。你想要的是 强制性锁定(Mandatory Locking)。
在 Node.js/Electron 的标准 fs 模块中,fs.open 打开的文件默认通常是允许“共享读写”的,这意味着虽然你打开了文件,外部程序(如记事本)可能仍然能读取甚至修改它(取决于 OS 及其共享标志)。
要实现“只能在你的 IDE 操作,外部完全无法重命名、删除、修改”,你需要绕过 Node.js 的 fs,直接调用操作系统的原生 API(Windows API)。
推荐方案:使用 koffi (高性能 FFI 库) 调用 Windows CreateFile API
目前没有现成的 npm 包能完美做到“递归锁定文件夹且不消耗系统资源”,因为这是操作系统层面的限制。但我可以为你写一个基于 koffi(现代版 ffi-napi,支持 Electron)的工具类,通过独占句柄来实现。
核心原理
调用 Windows 的 CreateFileW 函数,并将 dwShareMode 设置为 0(即不共享)。
- 只要你的 IDE 拿着这个句柄(Handle)不释放,任何其他进程(资源管理器、Git、Notepad)尝试访问该文件都会收到
Access Denied 或 File is being used by another process。
1. 安装依赖
你需要 koffi 来调用 C++ API。
2. 实现 HandleLockManager.ts
这个管理器负责打开文件/文件夹的独占句柄。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| import koffi from 'koffi' import path from 'path'
const lib = koffi.load('kernel32.dll')
const GENERIC_READ = 0x80000000 const GENERIC_WRITE = 0x40000000 const OPEN_EXISTING = 3 const FILE_ATTRIBUTE_NORMAL = 0x80 const FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 const INVALID_HANDLE_VALUE = -1n
const CreateFileW = lib.func('CreateFileW', 'void*', [ 'str16', 'uint32', 'uint32', 'void*', 'uint32', 'uint32', 'void*' ])
const CloseHandle = lib.func('CloseHandle', 'int', ['void*'])
export class HandleLockManager { private handles = new Map<string, any>()
lock(targetPath: string) { if (this.handles.has(targetPath)) return true
const shareMode = 0 const flags = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS
const handle = CreateFileW( targetPath, GENERIC_READ, shareMode, null, OPEN_EXISTING, flags, null )
if (BigInt(handle) === INVALID_HANDLE_VALUE) { console.error(`锁定失败: ${targetPath} (可能已被占用或权限不足)`) return false }
this.handles.set(targetPath, handle) console.log(`🔒 已独占锁定: ${targetPath}`) return true }
unlock(targetPath: string) { const handle = this.handles.get(targetPath) if (!handle) return
CloseHandle(handle) this.handles.delete(targetPath) console.log(`🔓 已释放句柄: ${targetPath}`) }
unlockAll() { for (const [path, handle] of this.handles) { CloseHandle(handle) } this.handles.clear() } }
export const handleLockManager = new HandleLockManager()
|
3. 如何使用
在你的主进程逻辑中集成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import { handleLockManager } from './HandleLockManager' import fs from 'fs' import path from 'path'
ipcMain.handle('open-project', async (e, projectDir) => { handleLockManager.lock(projectDir)
const lockRecursively = (dir: string) => { const items = fs.readdirSync(dir) for (const item of items) { if (['.git', 'node_modules'].includes(item)) continue; const fullPath = path.join(dir, item) handleLockManager.lock(fullPath) if (fs.statSync(fullPath).isDirectory()) { lockRecursively(fullPath) } } } lockRecursively(projectDir) return { success: true } })
ipcMain.handle('ide-write-file', async (e, { filePath, content }) => { handleLockManager.unlock(filePath) try { await fs.promises.writeFile(filePath, content) } finally { handleLockManager.lock(filePath) } })
app.on('will-quit', () => { handleLockManager.unlockAll() })
|
4. 这种方案的优缺点(必读)
优点:
- 绝对防御:这比
icacls 权限更狠。只要你的 IDE 运行着,外部用户在资源管理器里右键删除、重命名、或者用 Notepad 打开修改,Windows 都会直接弹窗“文件已在另一个程序中打开”,完全无法操作。
- 即时生效:不需要等待,调用即锁定。
- 崩溃安全:如果你的 Electron App 崩溃了(进程彻底结束),操作系统会自动回收所有句柄,锁会自动解开。不会像
icacls 那样留下永久的权限烂摊子。
缺点与挑战:
- 文件夹锁定的局限:
- 锁定一个文件夹的句柄,只能防止该文件夹本身被重命名或删除。
- 它不能阻止外部程序修改文件夹里面的文件。
- 解决方案:你必须像上面代码那样,递归遍历所有文件并对每一个文件都打开一个句柄。
- 句柄数量限制:
- 如果工程有 10 万个文件,打开 10 万个句柄可能会消耗大量内存或达到系统限制。
- 建议策略:只锁定当前显示的、重要的文件,或者锁定根目录和一二级目录,配合
chokidar 监听深层目录。或者只在“独占模式”下才递归锁。
- 副作用:
- Git 无法使用:你在 IDE 锁着文件的时候,外部的
git pull、git checkout 会全部失败。
- 编译无法使用:如果你在外部运行
npm run build,编译器会因为无法读取/写入文件而报错。
总结
如果你的需求是**“除了我的 IDE,谁也别想动这些文件,包括用户自己”,那么句柄锁定 (Handle Locking)** 是最正确的技术路线。
使用 koffi 调用 CreateFileW 并设置 dwShareMode = 0 是实现这一目标的终极武器。
Prev:
Next: 使用trae开发