From 185f343e68ca0b6f4b0e4509142412e79d21a416 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Tue, 18 Sep 2012 08:46:39 +1000 Subject: [PATCH] Working date rolling file stream --- lib/streams/BaseRollingFileStream.js | 24 ++++-- lib/streams/DateRollingFileStream.js | 79 ++++++++++++++++++- test/streams/DateRollingFileStream-test.js | 92 +++++++++++++++++++--- 3 files changed, 175 insertions(+), 20 deletions(-) diff --git a/lib/streams/BaseRollingFileStream.js b/lib/streams/BaseRollingFileStream.js index 64401b2..f3a4a22 100644 --- a/lib/streams/BaseRollingFileStream.js +++ b/lib/streams/BaseRollingFileStream.js @@ -2,18 +2,20 @@ var fs = require('fs'), util = require('util'); function debug(message) { -// util.debug(message); // console.log(message); } module.exports = BaseRollingFileStream; function BaseRollingFileStream(filename, options) { + +debug("In BaseRollingFileStream"); this.filename = filename; this.options = options || { encoding: 'utf8', mode: 0644, flags: 'a' }; this.rolling = false; this.writesWhileRolling = []; this.currentSize = 0; + this.rollBeforeWrite = false; function currentFileSize(file) { var fileSize = 0; @@ -32,7 +34,7 @@ function BaseRollingFileStream(filename, options) { } throwErrorIfArgumentsAreNotValid(); - +debug("Calling BaseRollingFileStream.super"); BaseRollingFileStream.super_.call(this, this.filename, this.options); this.currentSize = currentFileSize(this.filename); } @@ -67,18 +69,24 @@ BaseRollingFileStream.prototype.initRolling = function() { }; BaseRollingFileStream.prototype.write = function(data, encoding) { + var canWrite = false; if (this.rolling) { this.writesWhileRolling.push({ data: data, encoding: encoding }); - return false; } else { - var canWrite = BaseRollingFileStream.super_.prototype.write.call(this, data, encoding); - this.currentSize += data.length; - debug('current size = ' + this.currentSize); - if (this.shouldRoll()) { + if (this.rollBeforeWrite && this.shouldRoll()) { + this.writesWhileRolling.push({ data: data, encoding: encoding }); this.initRolling(); + } else { + canWrite = BaseRollingFileStream.super_.prototype.write.call(this, data, encoding); + this.currentSize += data.length; + debug('current size = ' + this.currentSize); + + if (!this.rollBeforeWrite && this.shouldRoll()) { + this.initRolling(); + } } - return canWrite; } + return canWrite; }; BaseRollingFileStream.prototype.shouldRoll = function() { diff --git a/lib/streams/DateRollingFileStream.js b/lib/streams/DateRollingFileStream.js index abb3282..9e7806e 100644 --- a/lib/streams/DateRollingFileStream.js +++ b/lib/streams/DateRollingFileStream.js @@ -1,16 +1,89 @@ var BaseRollingFileStream = require('./BaseRollingFileStream'), + format = require('../date_format'), + async = require('async'), + fs = require('fs'), util = require('util'); module.exports = DateRollingFileStream; -function DateRollingFileStream(filename, pattern, options) { - if (typeof(pattern) === 'object') { +function debug(message) { +// console.log(message); +} + +function DateRollingFileStream(filename, pattern, options, now) { + debug("Now is " + now); + if (pattern && typeof(pattern) === 'object') { + now = options; options = pattern; pattern = null; } - this.pattern = pattern || 'yyyy-mm-dd'; + this.pattern = pattern || '.yyyy-MM-dd'; + this.now = now || Date.now; + this.lastTimeWeWroteSomething = format.asString(this.pattern, new Date(this.now())); + debug("this.now is " + this.now + ", now is " + now); DateRollingFileStream.super_.call(this, filename, options); + this.rollBeforeWrite = true; } util.inherits(DateRollingFileStream, BaseRollingFileStream); + +DateRollingFileStream.prototype.shouldRoll = function() { + var lastTime = this.lastTimeWeWroteSomething, + thisTime = format.asString(this.pattern, new Date(this.now())); + + debug("DateRollingFileStream.shouldRoll with now = " + this.now() + ", thisTime = " + thisTime + ", lastTime = " + lastTime); + + this.lastTimeWeWroteSomething = thisTime; + this.previousTime = lastTime; + + return thisTime !== lastTime; +}; + +DateRollingFileStream.prototype.roll = function(filename, callback) { + var that = this, + newFilename = filename + this.previousTime; + + debug("Starting roll"); + debug("Queueing up data until we've finished rolling"); + debug("Flushing underlying stream"); + this.flush(); + + async.series([ + deleteAnyExistingFile, + renameTheCurrentFile, + openANewFile + ], callback); + + function deleteAnyExistingFile(cb) { + //on windows, you can get a EEXIST error if you rename a file to an existing file + //so, we'll try to delete the file we're renaming to first + fs.unlink(newFilename, function (err) { + //ignore err: if we could not delete, it's most likely that it doesn't exist + cb(); + }); + } + + function renameTheCurrentFile(cb) { + debug("Renaming the " + filename + " -> " + newFilename); + fs.rename(filename, newFilename, cb); + } + + function openANewFile(cb) { + debug("Opening a new file"); + fs.open( + filename, + that.options.flags, + that.options.mode, + function (err, fd) { + debug("opened new file"); + var oldLogFileFD = that.fd; + that.fd = fd; + that.writable = true; + fs.close(oldLogFileFD, cb); + } + ); + } + + +}; diff --git a/test/streams/DateRollingFileStream-test.js b/test/streams/DateRollingFileStream-test.js index b32ffbf..2977d09 100644 --- a/test/streams/DateRollingFileStream-test.js +++ b/test/streams/DateRollingFileStream-test.js @@ -1,14 +1,26 @@ var vows = require('vows'), assert = require('assert'), fs = require('fs'), - DateRollingFileStream = require('../../lib/streams').DateRollingFileStream; + DateRollingFileStream = require('../../lib/streams').DateRollingFileStream, + testTime = new Date(2012, 8, 12, 10, 37, 11); + +function cleanUp(filename) { + return function() { + fs.unlink(filename); + }; +} + +function now() { + return testTime.getTime(); +} vows.describe('DateRollingFileStream').addBatch({ 'arguments': { - topic: new DateRollingFileStream('test-date-rolling-file-stream', 'yyyy-mm-dd.hh'), + topic: new DateRollingFileStream(__dirname + '/test-date-rolling-file-stream-1', 'yyyy-mm-dd.hh'), + teardown: cleanUp(__dirname + '/test-date-rolling-file-stream-1'), 'should take a filename and a pattern and return a FileWriteStream': function(stream) { - assert.equal(stream.filename, 'test-date-rolling-file-stream'); + assert.equal(stream.filename, __dirname + '/test-date-rolling-file-stream-1'); assert.equal(stream.pattern, 'yyyy-mm-dd.hh'); assert.instanceOf(stream, fs.FileWriteStream); }, @@ -20,15 +32,17 @@ vows.describe('DateRollingFileStream').addBatch({ }, 'default arguments': { - topic: new DateRollingFileStream('test-date-rolling-file-stream'), + topic: new DateRollingFileStream(__dirname + '/test-date-rolling-file-stream-2'), + teardown: cleanUp(__dirname + '/test-date-rolling-file-stream-2'), - 'pattern should be yyyy-mm-dd': function(stream) { - assert.equal(stream.pattern, 'yyyy-mm-dd'); + 'pattern should be .yyyy-MM-dd': function(stream) { + assert.equal(stream.pattern, '.yyyy-MM-dd'); } }, 'with stream arguments': { - topic: new DateRollingFileStream('test-rolling-file-stream', 'yyyy-mm-dd', { mode: 0666 }), + topic: new DateRollingFileStream(__dirname + '/test-date-rolling-file-stream-3', 'yyyy-MM-dd', { mode: 0666 }), + teardown: cleanUp(__dirname + '/test-date-rolling-file-stream-3'), 'should pass them to the underlying stream': function(stream) { assert.equal(stream.mode, 0666); @@ -36,13 +50,73 @@ vows.describe('DateRollingFileStream').addBatch({ }, 'with stream arguments but no pattern': { - topic: new DateRollingFileStream('test-rolling-file-stream', { mode: 0666 }), + topic: new DateRollingFileStream(__dirname + '/test-date-rolling-file-stream-4', { mode: 0666 }), + teardown: cleanUp(__dirname + '/test-date-rolling-file-stream-4'), 'should pass them to the underlying stream': function(stream) { assert.equal(stream.mode, 0666); }, 'should use default pattern': function(stream) { - assert.equal(stream.pattern, 'yyyy-mm-dd'); + assert.equal(stream.pattern, '.yyyy-MM-dd'); + } + }, + + 'with a pattern of .yyyy-MM-dd': { + topic: function() { + var that = this, + stream = new DateRollingFileStream(__dirname + '/test-date-rolling-file-stream-5', '.yyyy-MM-dd', null, now); + stream.on("open", function() { + stream.write("First message\n"); + //wait for the file system to catch up with us + that.callback(null, stream); + }); + }, + teardown: cleanUp(__dirname + '/test-date-rolling-file-stream-5'), + + 'should create a file with the base name': { + topic: function(stream) { + fs.readFile(__dirname + '/test-date-rolling-file-stream-5', this.callback); + }, + 'file should contain first message': function(result) { + assert.equal(result.toString(), "First message\n"); + } + }, + + 'when the day changes': { + topic: function(stream) { + testTime = new Date(2012, 8, 13, 0, 10, 12); + stream.write("Second message\n"); + setTimeout(this.callback, 100); + }, + teardown: cleanUp(__dirname + '/test-date-rolling-file-stream-5.2012-09-12'), + + + 'the number of files': { + topic: function() { + fs.readdir(__dirname, this.callback); + }, + 'should be two': function(files) { + assert.equal(files.filter(function(file) { return file.indexOf('test-date-rolling-file-stream-5') > -1; }).length, 2); + } + }, + + 'the file without a date': { + topic: function() { + fs.readFile(__dirname + '/test-date-rolling-file-stream-5', this.callback); + }, + 'should contain the second message': function(contents) { + assert.equal(contents.toString(), "Second message\n"); + } + }, + + 'the file with the date': { + topic: function() { + fs.readFile(__dirname + '/test-date-rolling-file-stream-5.2012-09-12', this.callback); + }, + 'should contain the first message': function(contents) { + assert.equal(contents.toString(), "First message\n"); + } + } } }