Skip to content

Commit

Permalink
Merge branch 'no-dataloss-during-loaddatabase'
Browse files Browse the repository at this point in the history
  • Loading branch information
louischatriot committed Nov 28, 2013
2 parents 9f32b1e + f442b69 commit 2ffaa1b
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 8 deletions.
17 changes: 16 additions & 1 deletion lib/customUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
var crypto = require('crypto');
var crypto = require('crypto')
, fs = require('fs')
;

/**
* Return a random alphanumerical string of length len
Expand All @@ -16,4 +18,17 @@ function uid (len) {
}


/**
* Callback signature: err
*/
function ensureFileDoesntExist (file, callback) {
fs.exists(file, function (exists) {
if (!exists) { return callback(null); }

fs.unlink(file, function (err) { return callback(err); });
});
}


module.exports.uid = uid;
module.exports.ensureFileDoesntExist = ensureFileDoesntExist;
2 changes: 1 addition & 1 deletion lib/datastore.js
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,6 @@ Datastore.prototype._remove = function (query, options, cb) {
Datastore.prototype.remove = function () {
this.executor.push({ this: this, fn: this._remove, arguments: arguments });
};


module.exports = Datastore;
64 changes: 59 additions & 5 deletions lib/persistence.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var fs = require('fs')
, model = require('./model')
, async = require('async')
, mkdirp = require('mkdirp')
, customUtils = require('./customUtils')
;


Expand All @@ -23,6 +24,15 @@ function Persistence (options) {
this.db = options.db;
this.inMemoryOnly = this.db.inMemoryOnly;
this.filename = this.db.filename;

if (!this.inMemoryOnly && this.filename) {
if (this.filename.charAt(this.filename.length - 1) === '~') {
throw "The datafile name can't end with a ~, which is reserved for automatic backup files";
} else {
this.tempFilename = this.filename + '~';
this.oldFilename = this.filename + '~~';
}
}

// For NW apps, store data in the same directory where NW stores application data
if (this.filename && options.nodeWebkitAppName) {
Expand Down Expand Up @@ -85,13 +95,35 @@ Persistence.getNWAppFilename = function (appName, relativeFilename) {
Persistence.prototype.persistCachedDatabase = function (cb) {
var callback = cb || function () {}
, toPersist = ''
, self = this
;

if (this.inMemoryOnly) { return callback(null); }

this.db.getAllData().forEach(function (doc) {
toPersist += model.serialize(doc) + '\n';
});

fs.writeFile(this.filename, toPersist, function (err) { return callback(err); });

async.waterfall([
async.apply(customUtils.ensureFileDoesntExist, self.tempFilename)
, async.apply(customUtils.ensureFileDoesntExist, self.oldFilename)
, function (cb) {
fs.exists(self.filename, function (exists) {
if (exists) {
fs.rename(self.filename, self.oldFilename, function (err) { return cb(err); });
} else {
return cb();
}
});
}
, function (cb) {
fs.writeFile(self.tempFilename, toPersist, function (err) { return cb(err); });
}
, function (cb) {
fs.rename(self.tempFilename, self.filename, function (err) { return cb(err); });
}
, async.apply(customUtils.ensureFileDoesntExist, self.oldFilename)
], function (err) { if (err) { return callback(err); } else { return callback(null); } })
};


Expand Down Expand Up @@ -188,6 +220,30 @@ Persistence.treatRawData = function (rawData) {
};


/**
* Ensure that this.filename contains the most up-to-date version of the data
* Even if a loadDatabase crashed before
*/
Persistence.prototype.ensureDatafileIntegrity = function (callback) {
var self = this ;

fs.exists(self.filename, function (filenameExists) {
// Write was successful
if (filenameExists) { return callback(null); }

fs.exists(self.oldFilename, function (oldFilenameExists) {
// New database
if (!oldFilenameExists) {
return fs.writeFile(self.filename, '', 'utf8', function (err) { callback(err); });
}

// Write failed, use old version
fs.rename(self.oldFilename, self.filename, function (err) { return callback(err); });
});
});
};


/**
* Load the database
* This means pulling data out of the data file or creating it if it doesn't exist
Expand All @@ -208,9 +264,7 @@ Persistence.prototype.loadDatabase = function (cb) {
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); }); }

self.ensureDatafileIntegrity(function (exists) {
fs.readFile(self.filename, 'utf8', function (err, rawData) {
if (err) { return cb(err); }
var treatedData = Persistence.treatRawData(rawData);
Expand Down
35 changes: 35 additions & 0 deletions test/customUtil.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
var should = require('chai').should()
, assert = require('chai').assert
, customUtils = require('../lib/customUtils')
, fs = require('fs')
;


describe('customUtils', function () {

describe('ensureFileDoesntExist', function () {

it('Doesnt do anything if file already doesnt exist', function (done) {
customUtils.ensureFileDoesntExist('workspace/nonexisting', function (err) {
assert.isNull(err);
fs.existsSync('workspace/nonexisting').should.equal(false);
done();
});
});

it('Deletes file if it exists', function (done) {
fs.writeFileSync('workspace/existing', 'hello world', 'utf8');
fs.existsSync('workspace/existing').should.equal(true);

customUtils.ensureFileDoesntExist('workspace/existing', function (err) {
assert.isNull(err);
fs.existsSync('workspace/existing').should.equal(false);
done();
});
});

});



});
2 changes: 1 addition & 1 deletion test/mocha.opts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
--reporter spec
--timeout 2000
--timeout 10000
Loading

0 comments on commit 2ffaa1b

Please sign in to comment.