独占式文件锁定3

这是一个非常硬核的系统级需求。你想要的是 强制性锁定(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 DeniedFile is being used by another process

1. 安装依赖

你需要 koffi 来调用 C++ API。

1
npm install koffi

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
// src/main/HandleLockManager.ts
import koffi from 'koffi'
import path from 'path'

// === 1. 定义 Windows Kernel32 API ===
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 // koffi 使用 BigInt 处理指针/句柄

// 定义函数签名
const CreateFileW = lib.func('CreateFileW', 'void*', [
'str16', // lpFileName (宽字符路径)
'uint32', // dwDesiredAccess
'uint32', // dwShareMode (关键在这里!设为 0 表示独占)
'void*', // lpSecurityAttributes
'uint32', // dwCreationDisposition
'uint32', // dwFlagsAndAttributes
'void*' // hTemplateFile
])

const CloseHandle = lib.func('CloseHandle', 'int', ['void*'])

export class HandleLockManager {
// 存储路径到句柄的映射
private handles = new Map<string, any>()

/**
* 🔒 锁定单个文件或文件夹
* @param targetPath 绝对路径
*/
lock(targetPath: string) {
if (this.handles.has(targetPath)) return true // 已经锁了

// 1. 构造参数
// dwShareMode = 0 (表示禁止其他进程 读、写、删除)
const shareMode = 0

// dwFlagsAndAttributes: 如果是文件夹,必须加 BACKUP_SEMANTICS
const flags = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS

// 2. 调用 Windows API
const handle = CreateFileW(
targetPath,
GENERIC_READ, // 我们只需要读权限来持有句柄
shareMode, // 【核心】0 = 独占!拒绝任何共享
null,
OPEN_EXISTING,
flags,
null
)

// 3. 检查是否成功
if (BigInt(handle) === INVALID_HANDLE_VALUE) {
console.error(`锁定失败: ${targetPath} (可能已被占用或权限不足)`)
return false
}

// 4. 存入 Map
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
// main.ts
import { handleLockManager } from './HandleLockManager'
import fs from 'fs'
import path from 'path'

// 当打开工程时
ipcMain.handle('open-project', async (e, projectDir) => {

// 1. 锁定根文件夹 (防止根目录被重命名/删除)
handleLockManager.lock(projectDir)

// 2. 递归锁定所有子文件 (暴力锁定模式)
// 注意:这会消耗系统句柄资源,文件数上万可能会有性能问题
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 }
})

// 当你的 IDE 需要修改文件时 (重命名/写入)
ipcMain.handle('ide-write-file', async (e, { filePath, content }) => {
// 1. 必须先解锁!因为你自己持有的 Read 句柄也会阻止你自己的 Write 操作
// (除非你在 CreateFile 时指定了 WRITE 权限,但为了安全通常先解锁)
handleLockManager.unlock(filePath)

try {
// 2. 执行操作
await fs.promises.writeFile(filePath, content)
} finally {
// 3. 操作完立即锁回去
handleLockManager.lock(filePath)
}
})

// 应用退出时释放所有
app.on('will-quit', () => {
handleLockManager.unlockAll()
})

4. 这种方案的优缺点(必读)

优点:

  1. 绝对防御:这比 icacls 权限更狠。只要你的 IDE 运行着,外部用户在资源管理器里右键删除、重命名、或者用 Notepad 打开修改,Windows 都会直接弹窗“文件已在另一个程序中打开”,完全无法操作。
  2. 即时生效:不需要等待,调用即锁定。
  3. 崩溃安全:如果你的 Electron App 崩溃了(进程彻底结束),操作系统会自动回收所有句柄,锁会自动解开。不会像 icacls 那样留下永久的权限烂摊子。

缺点与挑战:

  1. 文件夹锁定的局限
    • 锁定一个文件夹的句柄,只能防止该文件夹本身被重命名或删除。
    • 它不能阻止外部程序修改文件夹里面的文件
    • 解决方案:你必须像上面代码那样,递归遍历所有文件并对每一个文件都打开一个句柄。
  2. 句柄数量限制
    • 如果工程有 10 万个文件,打开 10 万个句柄可能会消耗大量内存或达到系统限制。
    • 建议策略:只锁定当前显示的、重要的文件,或者锁定根目录和一二级目录,配合 chokidar 监听深层目录。或者只在“独占模式”下才递归锁。
  3. 副作用
    • Git 无法使用:你在 IDE 锁着文件的时候,外部的 git pullgit checkout 会全部失败。
    • 编译无法使用:如果你在外部运行 npm run build,编译器会因为无法读取/写入文件而报错。

总结

如果你的需求是**“除了我的 IDE,谁也别想动这些文件,包括用户自己”,那么句柄锁定 (Handle Locking)** 是最正确的技术路线。

使用 koffi 调用 CreateFileW 并设置 dwShareMode = 0 是实现这一目标的终极武器。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器