mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Ensure storage/util.writeFile handles concurrent write attempts
This commit is contained in:
parent
de63e17a4d
commit
2c1274ff76
@ -71,30 +71,31 @@ function readFile(path,backupPath,emptyResponse,type) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const writeFileLocks = {}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
* Write content to a file using UTF8 encoding.
|
* Write content to a file using UTF8 encoding.
|
||||||
* This forces a fsync before completing to ensure
|
* This forces a fsync before completing to ensure
|
||||||
* the write hits disk.
|
* the write hits disk.
|
||||||
*/
|
*/
|
||||||
writeFile: function(path,content,backupPath) {
|
writeFile: async function(path,content,backupPath) {
|
||||||
|
const reqId = Math.floor(Math.random()*1000)
|
||||||
|
if (!writeFileLocks[path]) {
|
||||||
|
writeFileLocks[path] = Promise.resolve()
|
||||||
|
}
|
||||||
|
const result = writeFileLocks[path].then(async () => {
|
||||||
var backupPromise;
|
var backupPromise;
|
||||||
if (backupPath && fs.existsSync(path)) {
|
if (backupPath && fs.existsSync(path)) {
|
||||||
backupPromise = fs.copy(path,backupPath);
|
await fs.copy(path,backupPath);
|
||||||
} else {
|
log.trace(`utils.writeFile - copied ${path} TO ${backupPath}`)
|
||||||
backupPromise = Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dirname = fspath.dirname(path);
|
const dirname = fspath.dirname(path);
|
||||||
const tempFile = `${path}.$$$`;
|
const tempFile = `${path}.$$$`;
|
||||||
|
await fs.ensureDir(dirname)
|
||||||
|
|
||||||
return backupPromise.then(() => {
|
await new Promise(function(resolve,reject) {
|
||||||
if (backupPath) {
|
|
||||||
log.trace(`utils.writeFile - copied ${path} TO ${backupPath}`)
|
|
||||||
}
|
|
||||||
return fs.ensureDir(dirname)
|
|
||||||
}).then(() => {
|
|
||||||
return new Promise(function(resolve,reject) {
|
|
||||||
var stream = fs.createWriteStream(tempFile);
|
var stream = fs.createWriteStream(tempFile);
|
||||||
stream.on('open',function(fd) {
|
stream.on('open',function(fd) {
|
||||||
stream.write(content,'utf8',function() {
|
stream.write(content,'utf8',function() {
|
||||||
@ -110,10 +111,11 @@ module.exports = {
|
|||||||
log.warn(log._("storage.localfilesystem.fsync-fail",{path: tempFile, message: err.toString()}));
|
log.warn(log._("storage.localfilesystem.fsync-fail",{path: tempFile, message: err.toString()}));
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
}).then(() => {
|
|
||||||
log.trace(`utils.writeFile - written content to ${tempFile}`)
|
log.trace(`utils.writeFile - written content to ${tempFile}`)
|
||||||
return new Promise(function(resolve,reject) {
|
|
||||||
|
await new Promise(function(resolve,reject) {
|
||||||
fs.rename(tempFile,path,err => {
|
fs.rename(tempFile,path,err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.warn(log._("storage.localfilesystem.fsync-fail",{path: path, message: err.toString()}));
|
log.warn(log._("storage.localfilesystem.fsync-fail",{path: path, message: err.toString()}));
|
||||||
@ -122,8 +124,10 @@ module.exports = {
|
|||||||
log.trace(`utils.writeFile - renamed ${tempFile} to ${path}`)
|
log.trace(`utils.writeFile - renamed ${tempFile} to ${path}`)
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
writeFileLocks[path] = result.catch(() => {})
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
readFile: readFile,
|
readFile: readFile,
|
||||||
|
|
||||||
|
@ -14,11 +14,33 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
**/
|
**/
|
||||||
|
|
||||||
var should = require("should");
|
const should = require("should");
|
||||||
var NR_TEST_UTILS = require("nr-test-utils");
|
const NR_TEST_UTILS = require("nr-test-utils");
|
||||||
var util = NR_TEST_UTILS.require("@node-red/runtime/lib/storage/localfilesystem/util");
|
const util = NR_TEST_UTILS.require("@node-red/runtime/lib/storage/localfilesystem/util");
|
||||||
|
const { mkdtemp, readFile } = require('fs/promises');
|
||||||
|
const { join } = require('path');
|
||||||
|
const { tmpdir } = require('os');
|
||||||
|
|
||||||
describe('storage/localfilesystem/util', function() {
|
describe('storage/localfilesystem/util', function() {
|
||||||
|
describe('writeFile', function () {
|
||||||
|
it('manages concurrent calls to modify the same file', async function () {
|
||||||
|
const testDirectory = await mkdtemp(join(tmpdir(), 'nr-test-'));
|
||||||
|
const testFile = join(testDirectory, 'foo.txt')
|
||||||
|
const testBackupFile = testFile + '.$$$'
|
||||||
|
|
||||||
|
let counter = 0
|
||||||
|
const promises = [
|
||||||
|
util.writeFile(testFile, `update-${counter++}`, testBackupFile ),
|
||||||
|
util.writeFile(testFile, `update-${counter++}`, testBackupFile ),
|
||||||
|
util.writeFile(testFile, `update-${counter++}`, testBackupFile )
|
||||||
|
]
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
|
const result = await readFile(testFile, { encoding: 'utf-8' })
|
||||||
|
result.should.equal('update-2')
|
||||||
|
})
|
||||||
|
})
|
||||||
describe('parseJSON', function() {
|
describe('parseJSON', function() {
|
||||||
it('returns parsed JSON', function() {
|
it('returns parsed JSON', function() {
|
||||||
var result = util.parseJSON('{"a":123}');
|
var result = util.parseJSON('{"a":123}');
|
||||||
|
Loading…
Reference in New Issue
Block a user