/** * Handle every persistence-related task * The interface Datastore expects to be implemented is * * Persistence.loadDatabase(callback) and callback has signature err * * Persistence.persistNewState(newDocs, callback) where newDocs is an array of documents and callback has signature err */ var fs = require('fs') , path = require('path') , model = require('./model') , async = require('async') , mkdirp = require('mkdirp') ; /** * Create a new Persistence object for database options.db * @param {Datastore} options.db * @param {Boolean} options.nodeWebkitAppName Optional, specify the name of your NW app if you want options.filename to be relative to the directory where * Node Webkit stores application data such as cookies and local storage (the best place to store data in my opinion) */ function Persistence (options) { this.db = options.db; this.inMemoryOnly = this.db.inMemoryOnly; this.filename = this.db.filename; // For NW apps, store data in the same directory where NW stores application data if (this.filename && options.nodeWebkitAppName) { this.filename = Persistence.getNWAppFilename(options.nodeWebkitAppName, this.filename); } // We keep internally the number of lines in the datafile // This will be used when/if I implement autocompacting when the datafile grows too big // For now it is not urgent as autocompaction happens upon every restart this.datafileSize = 0; }; /** * Check if a directory exists and create it on the fly if it is not the case * cb is optional, signature: err */ Persistence.ensureDirectoryExists = function (dir, cb) { var callback = cb || function () {} ; mkdirp(dir, function (err) { return callback(err); }); }; /** * Return the path the datafile if the given filename is relative to the directory where Node Webkit stores * data for this application. Probably the best place to store data */ Persistence.getNWAppFilename = function (appName, relativeFilename) { var home; switch (process.platform) { case 'win32': case 'win64': home = process.env.LOCALAPPDATA || process.env.APPDATA; if (!home) { throw "Couldn't find the base application data folder"; } home = path.join(home, appName); break; case 'darwin': home = process.env.HOME; if (!home) { throw "Couldn't find the base application data directory"; } home = path.join(home, 'Library', 'Application Support', appName); break; case 'linux': home = process.env.HOME; if (!home) { throw "Couldn't find the base application data directory"; } home = path.join(home, '.config', appName); break; default: throw "Can't use the Node Webkit relative path for platform " + process.platform; break; } return path.join(home, 'nedb-data', relativeFilename); } /** * Persist cached database * This serves as a compaction function since the cache always contains only the number of documents in the collection * while the data file is append-only so it may grow larger * @param {Function} cb Optional callback, signature: err */ Persistence.prototype.persistCachedDatabase = function (cb) { var callback = cb || function () {} , toPersist = '' ; this.db.getAllData().forEach(function (doc) { toPersist += model.serialize(doc) + '\n'; }); if (toPersist.length === 0) { return callback(null); } fs.writeFile(this.filename, toPersist, function (err) { return callback(err); }); }; /** * Persist new state for the given newDocs (can be insertion, update or removal) * Use an append-only format * @param {Array} newDocs Can be empty if no doc was updated/removed * @param {Function} cb Optional, signature: err */ Persistence.prototype.persistNewState = function (newDocs, cb) { var self = this , toPersist = '' , callback = cb || function () {} ; // In-memory only datastore if (self.inMemoryOnly) { return callback(null); } self.datafileSize += newDocs.length; newDocs.forEach(function (doc) { toPersist += model.serialize(doc) + '\n'; }); if (toPersist.length === 0) { return callback(null); } fs.appendFile(self.filename, toPersist, 'utf8', function (err) { return callback(err); }); }; /** * From a database's raw data, return the corresponding * machine understandable collection */ Persistence.treatRawData = function (rawData) { var data = rawData.split('\n') , dataById = {} , res = [] , i; for (i = 0; i < data.length; i += 1) { var doc; try { doc = model.deserialize(data[i]); if (doc._id) { if (doc.$$deleted === true) { delete dataById[doc._id]; } else { dataById[doc._id] = doc; } } } catch (e) { } } Object.keys(dataById).forEach(function (k) { res.push(dataById[k]); }); return res; }; /** * Load the database * This means pulling data out of the data file or creating it if it doesn't exist * Also, all data is persisted right away, which has the effect of compacting the database file * This operation is very quick at startup for a big collection (60ms for ~10k docs) * @param {Function} cb Optional callback, signature: err */ Persistence.prototype.loadDatabase = function (cb) { var callback = cb || function () {} , self = this ; self.db.resetIndexes(); self.datafileSize = 0; // In-memory only datastore if (self.inMemoryOnly) { return callback(null); } async.waterfall([ function (cb) { Persistence.ensureDirectoryExists(path.dirname(self.filename), function (err) { fs.exists(self.filename, function (exists) { if (!exists) { return fs.writeFile(self.filename, '', 'utf8', function (err) { cb(err); }); } fs.readFile(self.filename, 'utf8', function (err, rawData) { if (err) { return cb(err); } var treatedData = Persistence.treatRawData(rawData); try { self.db.resetIndexes(treatedData); } catch (e) { self.db.resetIndexes(); // Rollback any index which didn't fail self.datafileSize = 0; return cb(e); } self.datafileSize = treatedData.length; self.db.persistence.persistCachedDatabase(cb); }); }); }); } ], function (err) { if (err) { return callback(err); } self.db.executor.processBuffer(); return callback(null); }); }; // Interface module.exports = Persistence;