rotating-file-stream
Advanced tools
Comparing version 0.0.1 to 0.0.2
316
index.js
"use strict"; | ||
var fs = require("fs"); | ||
var util = require("util"); | ||
var Writable = require("stream").Writable; | ||
function checkMeasure(v, what, units) { | ||
var ret = {}; | ||
var RotatingFileStream = require("./constructor"); | ||
ret.num = parseInt(v); | ||
if(isNaN(ret.num)) | ||
throw new Error("Unknown 'options." + what + "' format: " + v); | ||
if(ret.num <= 0) | ||
throw new Error("A positive integer number is expected for 'options." + what + "'"); | ||
ret.unit = v.replace(/^[ 0]*/g, "").substr((ret.num + "").length, 1); | ||
if(ret.unit.length === 0) | ||
throw new Error("Missing unit for 'options." + what + "'"); | ||
if(! units[ret.unit]) | ||
throw new Error("Unknown 'options." + what + "' unit: " + ret.unit); | ||
return ret; | ||
function unexpected(msg) { | ||
throw new Error("Unexpected case ( https://www.npmjs.com/package/rotating-file-stream#unexpected ): " + msg); | ||
} | ||
var intervalUnits = { | ||
m: true, | ||
h: true, | ||
d: true | ||
}; | ||
RotatingFileStream.prototype._write = function(chunk, encoding, callback) { | ||
if(this.err) | ||
unexpected("_write after error"); | ||
function checkInterval(v) { | ||
var ret = checkMeasure(v, "interval", intervalUnits); | ||
if(this.callback) | ||
unexpected("_write before callback"); | ||
switch(ret.unit) { | ||
case "m": | ||
if(parseInt(60 / ret.num) * ret.num != 60) | ||
throw new Error("An integer divider of 60 is expected as minutes for 'options.interval'"); | ||
break; | ||
if(! this.stream) { | ||
this.buffer += chunk; | ||
this.callback = callback; | ||
case "h": | ||
if(parseInt(24 / ret.num) * ret.num != 24) | ||
throw new Error("An integer divider of 24 is expected as hours for 'options.interval'"); | ||
break; | ||
return; | ||
} | ||
return ret; | ||
} | ||
var self = this; | ||
var sizeUnits = { | ||
K: true, | ||
M: true, | ||
G: true | ||
}; | ||
this.size += chunk.length; | ||
this.stream.write(chunk, function(err) { | ||
if(err) | ||
return callback(err); | ||
function checkSize(v) { | ||
var ret = checkMeasure(v, "size", sizeUnits); | ||
if(self.options.size && self.size >= self.options.size) | ||
return self.rotate(callback); | ||
if(ret.unit == "K") | ||
return ret.num * 1024; | ||
callback(); | ||
}); | ||
}; | ||
if(ret.unit == "M") | ||
return ret.num * 1048576; | ||
RotatingFileStream.prototype._writev = function(chunks, callback) { | ||
if(this.err) | ||
unexpected("_writev after error"); | ||
return ret.num * 1073741824; | ||
} | ||
if(this.callback) | ||
unexpected("_writev before callback"); | ||
function checkOptions(options) { | ||
for(var opt in options) { | ||
var val = options[opt]; | ||
var typ = typeof val; | ||
if(! this.stream) | ||
unexpected("_writev while initial rotation"); | ||
switch(opt) { | ||
case "compress": | ||
if(! val) | ||
throw new Error("A value for 'options.compress' must be specified"); | ||
var buffer = ""; | ||
var enough = true; | ||
var i; | ||
var self = this; | ||
if(typ == "boolean") | ||
options.compress = function(src, dst) { return "cat " + src + " | gzip -t9 > " + dst; }; | ||
else | ||
if(typ == "string") { | ||
if(val != "bzip" && val != "gzip" && val != "zip") | ||
throw new Error("Don't know how to handle compression method: " + val); | ||
} | ||
else | ||
if(typ != "function") | ||
throw new Error("Don't know how to handle 'options.compress' type: " + typ); | ||
break; | ||
for(i = 0; i < chunks.length && enough; ++i) { | ||
buffer += chunks[i].chunk; | ||
case "highWaterMark": | ||
break; | ||
case "interval": | ||
if(typ != "string") | ||
throw new Error("Don't know how to handle 'options.interval' type: " + typ); | ||
options.interval = checkInterval(val); | ||
break; | ||
case "mode": | ||
break; | ||
case "size": | ||
if(typ != "string") | ||
throw new Error("Don't know how to handle 'options.size' type: " + typ); | ||
options.size = checkSize(val); | ||
break; | ||
default: | ||
throw new Error("Unknown option: " + opt); | ||
} | ||
if(this.options.size && (buffer.length + this.size >= this.options.size)) | ||
enough = false; | ||
} | ||
} | ||
function pad(num) { | ||
return (num + "").length == 1 ? "0" + num : num; | ||
} | ||
this.size += buffer.length; | ||
this.stream.write(buffer, function(err) { | ||
if(err) | ||
return self.error(err, callback); | ||
function createGenerator(filename) { | ||
return function(time, index) { | ||
if(! time) | ||
return filename; | ||
if(enough) | ||
return callback(); | ||
var month = time.getFullYear() + "" + pad(time.getMonth() + 1); | ||
var day = pad(time.getDate()); | ||
var hour = pad(time.getHours()); | ||
var minute = pad(time.getMinutes()); | ||
for(0; i < chunks.length; ++i) | ||
self.buffer += chunks[i].chunk; | ||
return month + day + "-" + hour + minute + "-" + pad(index) + "-" + filename; | ||
}; | ||
} | ||
self.rotate(callback); | ||
}); | ||
}; | ||
function RotatingFileStream(filename, options) { | ||
if(! (this instanceof RotatingFileStream)) | ||
return new RotatingFileStream(filename, options); | ||
RotatingFileStream.prototype.error = function(err, callback) { | ||
if(this.callback) | ||
callback = this.callback; | ||
var generator; | ||
var opt = {}; | ||
this.callback = null; | ||
if(typeof filename == "function") | ||
generator = filename; | ||
else | ||
if(typeof filename == "string") | ||
generator = createGenerator(filename); | ||
else | ||
throw new Error("Don't know how to handle 'filename' type: " + typeof filename); | ||
if(callback) | ||
return callback(err); | ||
if(! options) | ||
options = {}; | ||
else | ||
if(typeof options != "object") | ||
throw new Error("Don't know how to handle 'options' type: " + typeof options); | ||
this.emit("error", err); | ||
}; | ||
checkOptions(options); | ||
if(options.highWaterMark) | ||
opt.highWaterMark = options.highWaterMark; | ||
Writable.call(this); | ||
this.generator = generator; | ||
this.options = options; | ||
this.firstOpen(); | ||
} | ||
util.inherits(RotatingFileStream, Writable); | ||
RotatingFileStream.prototype.firstOpen = function() { | ||
@@ -185,5 +100,4 @@ try { | ||
// if file needs to be rotated at start time, do not open it: it will be opened by rotation | ||
if(this.firstRotation()) | ||
return; | ||
this.open(); | ||
}; | ||
@@ -199,3 +113,3 @@ | ||
if(e.code == "ENOENT") | ||
return false; | ||
return true; | ||
@@ -208,14 +122,118 @@ throw e; | ||
if(stats.size < this.options.size) | ||
return false; | ||
this.size = stats.size; | ||
this.rotate(); | ||
if((! this.options.size) || stats.size < this.options.size) | ||
return true; | ||
return true; | ||
process.nextTick(this.rotate.bind(this)); | ||
return false; | ||
}; | ||
RotatingFileStream.prototype.rotate = function() { | ||
RotatingFileStream.prototype.move = function(attempts) { | ||
if(! attempts) | ||
attempts = {}; | ||
var count = 0; | ||
for(var i in attempts) | ||
count += attempts[i]; | ||
if(count >= 1000) { | ||
var err = new Error("Too many destination file attempts"); | ||
err.attempts = attempts; | ||
return this.error(err, this.callback); | ||
} | ||
if(this.options.interval) | ||
throw new Error("not implemented yet"); | ||
var name = this.generator(this.rotation, count + 1); | ||
var self = this; | ||
fs.stat(name, function(err) { | ||
if((! err) || err.code != "ENOENT" ) { | ||
if(name in attempts) | ||
attempts[name]++; | ||
else | ||
attempts[name] = 1; | ||
return self.move(attempts); | ||
} | ||
if(self.options.compress) | ||
throw new Error("not implemented yet"); | ||
fs.rename(self.name, name, function(err) { | ||
if(err) | ||
return self.error(err, this.callback); | ||
self.emit("rotated", name); | ||
self.open(); | ||
}); | ||
}); | ||
}; | ||
RotatingFileStream.prototype.open = function() { | ||
var fd; | ||
var self = this; | ||
var callback = function(err) { | ||
var cb = self.callback; | ||
if(cb) { | ||
self.callback = null; | ||
return cb(err); | ||
} | ||
if(err) | ||
throw err; | ||
}; | ||
try { | ||
var options = { flags: "a" }; | ||
if("mode" in this.options) | ||
options.mode = this.options.mode; | ||
this.stream = fs.createWriteStream(this.name, options); | ||
} | ||
catch(e) { | ||
return callback(e); | ||
} | ||
if(! this.buffer.length) | ||
return callback(); | ||
this.stream.write(this.buffer, function(err) { | ||
if(err) | ||
return self.error(err, callback); | ||
callback(); | ||
}); | ||
this.size += this.buffer.length; | ||
this.buffer = ""; | ||
}; | ||
RotatingFileStream.prototype.rotate = function(callback) { | ||
if(callback) | ||
this.callback = callback; | ||
this.size = 0; | ||
this.rotation = new Date(); | ||
this.emit("rotation"); | ||
if(this.stream) { | ||
this.stream.on("finish", this.move.bind(this)); | ||
this.stream.end(); | ||
this.stream = null; | ||
} | ||
else | ||
this.move(); | ||
}; | ||
module.exports = RotatingFileStream; |
{ | ||
"name": "rotating-file-stream", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "Opens a stream.Writable to a file rotated by interval and/or size. A logrotate alternative.", | ||
"scripts": { | ||
"test": "echo '.jshintrc\\n.gitignore\\n.travis.yml\\ntest' > .npmignore ; cat .gitignore >> .npmignore\n ./node_modules/.bin/jshint index.js test || exit 1\n ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --recursive test" | ||
"test": "rm -rf *log ; echo '.jshintrc\\n.gitignore\\n.travis.yml\\ntest\\nsleep.js' > .npmignore ; cat .gitignore >> .npmignore\n ./node_modules/.bin/jshint index.js test || exit 1\n ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --recursive test" | ||
}, | ||
@@ -8,0 +8,0 @@ "bugs": "https://github.com/iccicci/rotating-file-stream/issues", |
@@ -28,3 +28,3 @@ # rotating-file-stream | ||
Please check the [TODO list](https://github.com/iccicci/rotating-file-stream#todo) to be aware of what is missing. | ||
Please check the [TODO list](#todo) to be aware of what is missing. | ||
@@ -56,4 +56,4 @@ ### Installation | ||
* time: {Date} The start time of rotation period. If __null__, the not rotated file name must be returned. | ||
* index {Number} The progressive index of rotation by size in the same rotation period. Starts from 1. | ||
* time: {Date} If rotation by interval is enabled, the start time of rotation period, otherwise the time when rotation job started. If __null__, the not rotated file name must be returned. | ||
* index {Number} The progressive index of rotation by size in the same rotation period. | ||
@@ -76,3 +76,4 @@ An example of a complex rotated file name generator function could be: | ||
return "/storage/" + month + "/" + month + day + "-" + hour + minute + "-" + index + "-" + filename; | ||
return "/storage/" + month + "/" + | ||
month + day + "-" + hour + minute + "-" + index + "-" + filename; | ||
} | ||
@@ -88,3 +89,2 @@ | ||
__Note:__ | ||
If part of returned destination path does not exists, the rotation job will try to create it. | ||
@@ -104,2 +104,3 @@ | ||
* __B__: Bites | ||
* __K__: KiloBites | ||
@@ -125,2 +126,3 @@ * __M__: MegaBytes | ||
* __s__: seconds. Accepts integer divider of 60. | ||
* __m__: minutes. Accepts integer divider of 60. | ||
@@ -151,6 +153,8 @@ * __h__: hours. Accepts integer divider of 24. | ||
* gzip | ||
* zip | ||
To enable external compression, a _function_ can be used or simple the _boolean_ __true__ value to use default | ||
external compression. The two following code snippets have exactly the same effect: | ||
external compression. | ||
The function should accept _source_ and _dest_ file names and must return the shell command to be executed to | ||
archive the file. | ||
The two following code snippets have exactly the same effect: | ||
@@ -169,4 +173,4 @@ ```javascript | ||
size: '10M', | ||
compress: function(src, dst) { | ||
return "cat " + src + " | gzip -t9 > " + dst; | ||
compress: function(source, dest) { | ||
return "cat " + source + " | gzip -t9 > " + dest; | ||
} | ||
@@ -176,2 +180,6 @@ }); | ||
__Note:__ | ||
The shell command to archive the rotated file should not remove the source file, it will be removed by the package | ||
if archive job complete with success. | ||
### Events | ||
@@ -211,6 +219,23 @@ | ||
To not waste CPU power checking size for rotation at each _write_, a timer is set up to check size at | ||
every second. This means that rotated file size will be a bit greater than how much specified with | ||
__options.size__ parameter. | ||
### Unexpected | ||
``` | ||
If I understood correctly, there are some case which should never happen. | ||
Anyway I want to be sure, so I decided to throw an Error if code runs | ||
through one of these cases. | ||
If it happen that you catch one of these, please make me aware of that as | ||
soon as possible in order to handle the case. | ||
The author | ||
``` | ||
### Compatibility | ||
This package is written following __Node.js 4.0__ specifications always taking care about backward | ||
compatibility. The package it tested under following versions: | ||
* 4.0 | ||
* 0.12 | ||
* 0.11 | ||
* 0.10 | ||
### Licence | ||
@@ -226,12 +251,13 @@ | ||
* Write tests | ||
* Write code | ||
* Emit events | ||
* Rotate by interval | ||
* Create missing directories in paths | ||
* External compression | ||
* Internal compression gzip | ||
* Internal compression bzip | ||
* Internal compression zip | ||
* Test all error case handling | ||
### Changelog | ||
* 2015-09-17 - v0.0.2 | ||
* Rotation by size | ||
* 2015-09-14 - v0.0.1 | ||
@@ -238,0 +264,0 @@ * README.md |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
18662
5
309
256
3
1