|
|
@ -7,16 +7,21 @@ |
|
|
|
* It's essentially fs, mkdirp and crash safe write and read functions |
|
|
|
* It's essentially fs, mkdirp and crash safe write and read functions |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const fs = require('fs') |
|
|
|
const fs = require('fs') |
|
|
|
|
|
|
|
const fsPromises = require('fs/promises') |
|
|
|
const path = require('path') |
|
|
|
const path = require('path') |
|
|
|
const async = require('async') |
|
|
|
const { callbackify, promisify } = require('util') |
|
|
|
const storage = {} |
|
|
|
const storage = {} |
|
|
|
const { Readable } = require('stream') |
|
|
|
const { Readable } = require('stream') |
|
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line node/no-callback-literal
|
|
|
|
// eslint-disable-next-line node/no-callback-literal
|
|
|
|
storage.exists = (path, cb) => fs.access(path, fs.constants.F_OK, (err) => { cb(!err) }) |
|
|
|
storage.exists = (path, cb) => fs.access(path, fs.constants.F_OK, (err) => { cb(!err) }) |
|
|
|
|
|
|
|
storage.existsAsync = path => fsPromises.access(path, fs.constants.F_OK).then(() => true, () => false) |
|
|
|
storage.rename = fs.rename |
|
|
|
storage.rename = fs.rename |
|
|
|
|
|
|
|
storage.renameAsync = fsPromises.rename |
|
|
|
storage.writeFile = fs.writeFile |
|
|
|
storage.writeFile = fs.writeFile |
|
|
|
|
|
|
|
storage.writeFileAsync = fsPromises.writeFile |
|
|
|
storage.unlink = fs.unlink |
|
|
|
storage.unlink = fs.unlink |
|
|
|
|
|
|
|
storage.unlinkAsync = fsPromises.unlink |
|
|
|
storage.appendFile = fs.appendFile |
|
|
|
storage.appendFile = fs.appendFile |
|
|
|
storage.readFile = fs.readFile |
|
|
|
storage.readFile = fs.readFile |
|
|
|
storage.readFileStream = fs.createReadStream |
|
|
|
storage.readFileStream = fs.createReadStream |
|
|
@ -25,21 +30,21 @@ storage.mkdir = fs.mkdir |
|
|
|
/** |
|
|
|
/** |
|
|
|
* Explicit name ... |
|
|
|
* Explicit name ... |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
storage.ensureFileDoesntExist = (file, callback) => { |
|
|
|
storage.ensureFileDoesntExistAsync = async file => { |
|
|
|
storage.exists(file, exists => { |
|
|
|
if (await storage.existsAsync(file)) await storage.unlinkAsync(file) |
|
|
|
if (!exists) return callback(null) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
storage.unlink(file, err => callback(err)) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
storage.ensureFileDoesntExist = (file, callback) => callbackify(storage.ensureFileDoesntExistAsync)(file, err => callback(err)) |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Flush data in OS buffer to storage if corresponding option is set |
|
|
|
* Flush data in OS buffer to storage if corresponding option is set |
|
|
|
* @param {String} options.filename |
|
|
|
* @param {String} options.filename |
|
|
|
* @param {Boolean} options.isDir Optional, defaults to false |
|
|
|
* @param {Boolean} options.isDir Optional, defaults to false |
|
|
|
* If options is a string, it is assumed that the flush of the file (not dir) called options was requested |
|
|
|
* If options is a string, it is assumed that the flush of the file (not dir) called options was requested |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
storage.flushToStorage = (options, callback) => { |
|
|
|
storage.flushToStorage = (options, callback) => callbackify(storage.flushToStorageAsync)(options, callback) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
storage.flushToStorageAsync = async (options) => { |
|
|
|
let filename |
|
|
|
let filename |
|
|
|
let flags |
|
|
|
let flags |
|
|
|
if (typeof options === 'string') { |
|
|
|
if (typeof options === 'string') { |
|
|
@ -62,23 +67,29 @@ storage.flushToStorage = (options, callback) => { |
|
|
|
* database is loaded and a crash happens. |
|
|
|
* database is loaded and a crash happens. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
fs.open(filename, flags, (err, fd) => { |
|
|
|
let fd, errorOnFsync, errorOnClose |
|
|
|
if (err) { |
|
|
|
try { |
|
|
|
return callback((err.code === 'EISDIR' && options.isDir) ? null : err) |
|
|
|
fd = await fsPromises.open(filename, flags) |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
await fd.sync() |
|
|
|
|
|
|
|
} catch (errFS) { |
|
|
|
|
|
|
|
errorOnFsync = errFS |
|
|
|
} |
|
|
|
} |
|
|
|
fs.fsync(fd, errFS => { |
|
|
|
} catch (error) { |
|
|
|
fs.close(fd, errC => { |
|
|
|
if (error.code !== 'EISDIR' || !options.isDir) throw error |
|
|
|
if ((errFS || errC) && !((errFS.code === 'EPERM' || errFS.code === 'EISDIR') && options.isDir)) { |
|
|
|
} finally { |
|
|
|
const e = new Error('Failed to flush to storage') |
|
|
|
try { |
|
|
|
e.errorOnFsync = errFS |
|
|
|
await fd.close() |
|
|
|
e.errorOnClose = errC |
|
|
|
} catch (errC) { |
|
|
|
return callback(e) |
|
|
|
errorOnClose = errC |
|
|
|
} else { |
|
|
|
} |
|
|
|
return callback(null) |
|
|
|
} |
|
|
|
} |
|
|
|
if ((errorOnFsync || errorOnClose) && !((errorOnFsync.code === 'EPERM' || errorOnClose.code === 'EISDIR') && options.isDir)) { |
|
|
|
}) |
|
|
|
const e = new Error('Failed to flush to storage') |
|
|
|
}) |
|
|
|
e.errorOnFsync = errorOnFsync |
|
|
|
}) |
|
|
|
e.errorOnClose = errorOnClose |
|
|
|
|
|
|
|
throw e |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
@ -109,6 +120,8 @@ storage.writeFileLines = (filename, lines, callback = () => {}) => { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
storage.writeFileLinesAsync = (filename, lines) => promisify(storage.writeFileLines)(filename, lines) |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Fully write or rewrite the datafile, immune to crashes during the write operation (data will not be lost) |
|
|
|
* Fully write or rewrite the datafile, immune to crashes during the write operation (data will not be lost) |
|
|
|
* @param {String} filename |
|
|
|
* @param {String} filename |
|
|
@ -116,25 +129,24 @@ storage.writeFileLines = (filename, lines, callback = () => {}) => { |
|
|
|
* @param {Function} callback Optional callback, signature: err |
|
|
|
* @param {Function} callback Optional callback, signature: err |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
storage.crashSafeWriteFileLines = (filename, lines, callback = () => {}) => { |
|
|
|
storage.crashSafeWriteFileLines = (filename, lines, callback = () => {}) => { |
|
|
|
|
|
|
|
callbackify(storage.crashSafeWriteFileLinesAsync)(filename, lines, callback) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
storage.crashSafeWriteFileLinesAsync = async (filename, lines) => { |
|
|
|
const tempFilename = filename + '~' |
|
|
|
const tempFilename = filename + '~' |
|
|
|
|
|
|
|
|
|
|
|
async.waterfall([ |
|
|
|
await storage.flushToStorageAsync({ filename: path.dirname(filename), isDir: true }) |
|
|
|
async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true }), |
|
|
|
|
|
|
|
cb => { |
|
|
|
const exists = await storage.existsAsync(filename) |
|
|
|
storage.exists(filename, exists => { |
|
|
|
if (exists) await storage.flushToStorageAsync({ filename }) |
|
|
|
if (exists) storage.flushToStorage(filename, err => cb(err)) |
|
|
|
|
|
|
|
else return cb() |
|
|
|
await storage.writeFileLinesAsync(tempFilename, lines) |
|
|
|
}) |
|
|
|
|
|
|
|
}, |
|
|
|
await storage.flushToStorageAsync(tempFilename) |
|
|
|
cb => { |
|
|
|
|
|
|
|
storage.writeFileLines(tempFilename, lines, cb) |
|
|
|
await storage.renameAsync(tempFilename, filename) |
|
|
|
}, |
|
|
|
|
|
|
|
async.apply(storage.flushToStorage, tempFilename), |
|
|
|
await storage.flushToStorageAsync({ filename: path.dirname(filename), isDir: true }) |
|
|
|
cb => { |
|
|
|
|
|
|
|
storage.rename(tempFilename, filename, err => cb(err)) |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
async.apply(storage.flushToStorage, { filename: path.dirname(filename), isDir: true }) |
|
|
|
|
|
|
|
], err => callback(err)) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
@ -142,21 +154,20 @@ storage.crashSafeWriteFileLines = (filename, lines, callback = () => {}) => { |
|
|
|
* @param {String} filename |
|
|
|
* @param {String} filename |
|
|
|
* @param {Function} callback signature: err |
|
|
|
* @param {Function} callback signature: err |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
storage.ensureDatafileIntegrity = (filename, callback) => { |
|
|
|
storage.ensureDatafileIntegrity = (filename, callback) => callbackify(storage.ensureDatafileIntegrityAsync)(filename, callback) |
|
|
|
const tempFilename = filename + '~' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
storage.exists(filename, filenameExists => { |
|
|
|
storage.ensureDatafileIntegrityAsync = async filename => { |
|
|
|
// Write was successful
|
|
|
|
const tempFilename = filename + '~' |
|
|
|
if (filenameExists) return callback(null) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
storage.exists(tempFilename, oldFilenameExists => { |
|
|
|
const filenameExists = await storage.existsAsync(filename) |
|
|
|
// New database
|
|
|
|
// Write was successful
|
|
|
|
if (!oldFilenameExists) return storage.writeFile(filename, '', 'utf8', err => { callback(err) }) |
|
|
|
if (filenameExists) return |
|
|
|
|
|
|
|
|
|
|
|
// Write failed, use old version
|
|
|
|
const oldFilenameExists = await storage.existsAsync(tempFilename) |
|
|
|
storage.rename(tempFilename, filename, err => callback(err)) |
|
|
|
// New database
|
|
|
|
}) |
|
|
|
if (!oldFilenameExists) await storage.writeFileAsync(filename, '', 'utf8') |
|
|
|
}) |
|
|
|
// Write failed, use old version
|
|
|
|
|
|
|
|
else await storage.renameAsync(tempFilename, filename) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Interface
|
|
|
|
// Interface
|
|
|
|