109 lines
3.3 KiB
JavaScript
109 lines
3.3 KiB
JavaScript
'use strict'
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
class FileLock {
|
|
constructor() {
|
|
this.lockedFiles = new Map()
|
|
}
|
|
|
|
/**
|
|
* Acquire a file-based lock
|
|
* @param {string} filePath - The path of the file to lock
|
|
* @returns {Promise<void>}
|
|
*/
|
|
acquire(filePath) {
|
|
return new Promise((resolve, reject) => {
|
|
const lockFilePath = this._getLockFilePath(filePath)
|
|
|
|
// Try to acquire lock immediately if not locked
|
|
if (!this.lockedFiles.has(filePath)) {
|
|
this._createLockFile(lockFilePath, filePath, resolve, reject)
|
|
} else {
|
|
// Add to queue if already locked
|
|
const queue = this.lockedFiles.get(filePath).queue
|
|
queue.push({ resolve, reject })
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Release a file-based lock
|
|
* @param {string} filePath - The path of the file to unlock
|
|
*/
|
|
release(filePath) {
|
|
if (!this.lockedFiles.has(filePath)) {
|
|
// Releasing an unlocked file is a no-op
|
|
return
|
|
}
|
|
|
|
const lockInfo = this.lockedFiles.get(filePath)
|
|
const lockFilePath = this._getLockFilePath(filePath)
|
|
|
|
// Remove lock file
|
|
try {
|
|
if (fs.existsSync(lockFilePath)) {
|
|
fs.unlinkSync(lockFilePath)
|
|
}
|
|
} catch (err) {
|
|
// Ignore errors during unlock
|
|
}
|
|
|
|
// Check if there are queued requests
|
|
if (lockInfo.queue.length > 0) {
|
|
// Process the next queued request
|
|
const nextRequest = lockInfo.queue.shift()
|
|
this._createLockFile(lockFilePath, filePath, nextRequest.resolve, nextRequest.reject)
|
|
} else {
|
|
// No more queued requests, remove from locked files
|
|
this.lockedFiles.delete(filePath)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a lock file
|
|
* @private
|
|
*/
|
|
_createLockFile(lockFilePath, filePath, resolve, reject) {
|
|
try {
|
|
// Use wx flag to atomically create the lock file
|
|
// If the file already exists, this will throw an error
|
|
fs.writeFileSync(lockFilePath, process.pid.toString(), { flag: 'wx' })
|
|
|
|
// Initialize queue for this file if not exists
|
|
if (!this.lockedFiles.has(filePath)) {
|
|
this.lockedFiles.set(filePath, {
|
|
lockFilePath: lockFilePath,
|
|
queue: []
|
|
})
|
|
}
|
|
|
|
resolve()
|
|
} catch (err) {
|
|
if (err.code === 'EEXIST') {
|
|
// Lock file already exists, add to queue
|
|
if (!this.lockedFiles.has(filePath)) {
|
|
this.lockedFiles.set(filePath, {
|
|
lockFilePath: lockFilePath,
|
|
queue: []
|
|
})
|
|
}
|
|
this.lockedFiles.get(filePath).queue.push({ resolve, reject })
|
|
} else {
|
|
reject(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get lock file path from original file path
|
|
* @private
|
|
*/
|
|
_getLockFilePath(filePath) {
|
|
const resolvedPath = path.resolve(filePath)
|
|
return `${resolvedPath}.lock`
|
|
}
|
|
}
|
|
|
|
module.exports = FileLock |