nestdb
Version:
The JavaScript Database for Node.js, nwjs, Electron, and the browser
156 lines (129 loc) • 4.49 kB
JavaScript
/**
* Way data is stored for this database
* For a Node.js/Node Webkit database it's the file system
* For a browser-side database it's localforage which chooses the best option depending on user browser (IndexedDB then WebSQL then localStorage)
*
* This version is the Node.js/Node Webkit version
* It's essentially fs, mkdirp and crash safe write and read functions
*/
// Node.js core modules
var fs = require('fs')
, path = require('path')
// Userland modules
, mkdirp = require('mkdirp')
, async = require('async')
// Local variables
, storage = {}
;
/**
* Read the file asynchronously, providing the data to a callback
* @public
*/
storage.read = function (file, callback) {
fs.readFile(file, { encoding: 'utf8' }, callback);
};
/**
* Append new data to the file asynchronously
* @public
*/
storage.append = function (file, data, callback) {
fs.appendFile(file, data, { encoding: 'utf8' }, callback);
};
/**
* Delete the file
* @public
*/
storage.remove = function (file, callback) {
fs.exists(file, function (exists) {
if (!exists) { return callback(null); }
fs.unlink(file, function (err) { return callback(err); });
});
};
/**
* Flush data in OS buffer to storage if corresponding option is set
* @param {String} options.filename
* @param {Boolean} options.isDir Optional, defaults to false
* @private
*/
function flushToStorage(options, callback) {
var filename = options.filename
, flags = options.isDir ? 'r' : 'r+';
// Windows can't fsync (FlushFileBuffers) directories. We can live with this as it cannot cause 100% dataloss
// except in the very rare event of the first time database is loaded and a crash happens
if (flags === 'r' && (process.platform === 'win32' || process.platform === 'win64')) { return callback(null); }
fs.open(filename, flags, function (err, fd) {
if (err) { return callback(err); }
fs.fsync(fd, function (errFS) {
fs.close(fd, function (errC) {
if (errFS || errC) {
var e = new Error('Failed to flush to storage');
e.errorOnFsync = errFS;
e.errorOnClose = errC;
return callback(e);
} else {
return callback(null);
}
});
});
});
}
/**
* Fully write or rewrite the datafile, immune to crashes during the write operation (data will not be lost)
* @param {String} filename
* @param {String} data
* @param {Function} cb Optional callback, signature: err
*/
storage.write = function (filename, data, cb) {
var callback = cb || function () {}
, tempFilename = filename + '~';
async.waterfall([
async.apply(flushToStorage, { filename: path.dirname(filename), isDir: true })
, function (cb) {
fs.exists(filename, function (exists) {
if (exists) {
flushToStorage({ filename: filename }, function (err) { return cb(err); });
} else {
return cb();
}
});
}
, function (cb) {
fs.writeFile(tempFilename, data, { encoding: 'utf8' }, function (err) { return cb(err); });
}
, async.apply(flushToStorage, { filename: tempFilename })
, function (cb) {
fs.rename(tempFilename, filename, function (err) { return cb(err); });
}
, async.apply(flushToStorage, { filename: path.dirname(filename), isDir: true })
], function (err) { return callback(err); })
};
/**
* Ensure the datafile contains all the data, even if there was a crash during a full file write,
* or else create a new empty datafile and any missing ancestor directories in its path
* @param {String} filename
* @param {Function} callback signature: err
*/
storage.init = function (filename, callback) {
var tempFilename = filename + '~';
fs.exists(filename, function (filenameExists) {
// Write was successful
if (filenameExists) { return callback(null); }
fs.exists(tempFilename, function (oldFilenameExists) {
// Write failed, use old version
if (oldFilenameExists) {
return fs.rename(tempFilename, filename, function (err) { return callback(err); });
}
// New database!
// Ensure that all of its ancestor directories exist
mkdirp(path.dirname(filename), function (err) {
if (err) {
return callback(err);
}
// And then create an empty datafile
return fs.writeFile(filename, '', { encoding: 'utf8' }, function (err) { callback(err); });
});
});
});
};
// Interface
module.exports = storage;