Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

rotating-file-stream

Package Overview
Dependencies
Maintainers
2
Versions
67
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

rotating-file-stream - npm Package Compare versions

Comparing version 1.4.6 to 2.0.0

4

CHANGELOG.md

@@ -0,1 +1,5 @@

- 2019-11-24 - v2.0.0
- complete refactoring with TypeScript
- full Windows compliance (at least all tests are OK)
- file is recreated if externally removed while logging
- 2019-10-20 - v1.4.6

@@ -2,0 +6,0 @@ - tests fix

@@ -1,21 +0,107 @@

import { WriteStream } from "fs";
export interface RfsOptions {
compress?: string | Function | boolean;
highWaterMark?: number;
history?: string;
immutable?: boolean;
initialRotation?: boolean;
interval?: string;
maxFiles?: number;
maxSize?: string;
mode?: number;
path?: string;
rotate?: number;
rotationTime?: boolean;
size?: string;
/// <reference types="node" />
import { Writable } from "stream";
export declare type Compressor = (source: string, dest: string) => string;
export declare type Generator = (time: number | Date, index?: number) => string;
export interface Options {
compress?: boolean | string | Compressor;
encoding?: string;
history?: string;
immutable?: boolean;
initialRotation?: boolean;
interval?: string;
intervalBoundary?: boolean;
maxFiles?: number;
maxSize?: string;
mode?: number;
path?: string;
rotate?: number;
size?: string;
}
declare function RotatingFileStream(fileName: string | Function, options: RfsOptions): WriteStream;
export default RotatingFileStream;
interface Opts {
compress?: string | Compressor;
encoding?: string;
history?: string;
immutable?: boolean;
initialRotation?: boolean;
interval?: {
num: number;
unit: string;
};
intervalBoundary?: boolean;
maxFiles?: number;
maxSize?: number;
mode?: number;
path?: string;
rotate?: number;
size?: number;
}
declare type Callback = (error?: Error) => void;
interface Chunk {
chunk: Buffer;
encoding: string;
next: Chunk;
}
export declare class RotatingFileStream extends Writable {
private createGzip;
private destroyer;
private error;
private exec;
private filename;
private finished;
private fsClose;
private fsCreateReadStream;
private fsCreateWriteStream;
private fsMkdir;
private fsOpen;
private fsReadFile;
private fsRename;
private fsStat;
private fsUnlink;
private fsWrite;
private fsWriteFile;
private generator;
private last;
private maxTimeout;
private next;
private opened;
private options;
private prev;
private rotatedName;
private rotation;
private size;
private stream;
private timer;
constructor(generator: Generator, options: Opts);
_destroy(error: Error, callback: Callback): void;
_final(callback: Callback): void;
_write(chunk: Buffer, encoding: string, callback: Callback): void;
_writev(chunks: Chunk[], callback: Callback): void;
private rewrite;
private init;
private makePath;
private reopen;
private reclose;
private now;
private rotate;
private findName;
private move;
private touch;
private classical;
private clear;
private intervalBoundsBig;
private intervalBounds;
private interval;
private compress;
private external;
private gzip;
private rotated;
private history;
private historyGather;
private historyRemove;
private historyCheckFiles;
private historyCheckSize;
private historyWrite;
private immutate;
}
export declare function createStream(filename: string | Generator, options?: Options): RotatingFileStream;
export {};

988

index.js
"use strict";
var compress = require("./compress");
var fs = require("fs");
var interval = require("./interval");
var path = require("path");
var util = require("util");
var utils = require("./utils");
var Writable = require("stream").Writable;
function RotatingFileStream(filename, options) {
if(! (this instanceof RotatingFileStream)) return new RotatingFileStream(filename, options);
options = utils.checkOptions(options);
if(typeof filename === "function") this.generator = filename;
else if(typeof filename === "string")
if(options.rotate) this.generator = utils.createClassical(filename);
else this.generator = utils.createGenerator(filename);
else throw new Error("Don't know how to handle 'filename' type: " + typeof filename);
if(options.path) {
var generator = this.generator;
this.generator = function(time, index) {
return path.join(options.path, generator(time, index));
};
}
var opt = {};
if(options.highWaterMark) opt.highWaterMark = options.highWaterMark;
if(options.mode) opt.mode = options.mode;
Writable.call(this, opt);
this.chunks = [];
this.options = options;
this.size = 0;
this.write = this.write; // https://github.com/iccicci/rotating-file-stream/issues/19
utils.setEvents(this);
process.nextTick(this.firstOpen.bind(this));
Object.defineProperty(exports, "__esModule", { value: true });
const child_process_1 = require("child_process");
const zlib_1 = require("zlib");
const stream_1 = require("stream");
const fs_1 = require("fs");
const path_1 = require("path");
const util_1 = require("util");
class RotatingFileStreamError extends Error {
constructor() {
super("Too many destination file attempts");
this.code = "RFS-TOO-MANY";
}
}
util.inherits(RotatingFileStream, Writable);
RotatingFileStream.prototype._close = function(done) {
if(this.stream) {
this.stream.on("finish", done);
this.stream.end();
this.stream = null;
}
else done();
class RotatingFileStream extends stream_1.Writable {
constructor(generator, options) {
const { encoding, history, maxFiles, maxSize, path } = options;
super({ decodeStrings: true, defaultEncoding: encoding });
this.createGzip = zlib_1.createGzip;
this.exec = child_process_1.exec;
this.filename = path + generator(null);
this.fsClose = fs_1.close;
this.fsCreateReadStream = fs_1.createReadStream;
this.fsCreateWriteStream = fs_1.createWriteStream;
this.fsMkdir = fs_1.mkdir;
this.fsOpen = fs_1.open;
this.fsReadFile = fs_1.readFile;
this.fsRename = fs_1.rename;
this.fsStat = fs_1.stat;
this.fsUnlink = fs_1.unlink;
this.fsWrite = fs_1.write;
this.fsWriteFile = fs_1.writeFile;
this.generator = generator;
this.maxTimeout = 2147483640;
this.options = options;
if (maxFiles || maxSize)
options.history = path + (history ? history : this.generator(null) + ".txt");
this.on("close", () => (this.finished ? null : this.emit("finish")));
this.on("finish", () => (this.finished = this.clear()));
process.nextTick(() => this.init(error => {
this.error = error;
if (this.opened)
this.opened();
}));
}
_destroy(error, callback) {
const destroyer = () => {
this.clear();
this.reclose(() => { });
};
if (this.stream)
destroyer();
else
this.destroyer = destroyer;
callback(error);
}
_final(callback) {
if (this.stream)
return this.stream.end(callback);
callback();
}
_write(chunk, encoding, callback) {
this.rewrite({ chunk, encoding, next: null }, callback);
}
_writev(chunks, callback) {
this.rewrite(chunks[0], callback);
}
rewrite(chunk, callback) {
const destroy = (error) => {
this.destroy();
return callback(error);
};
const rewrite = () => {
if (this.destroyed)
return callback(this.error);
if (this.error)
return destroy(this.error);
const done = (error) => {
if (error)
return destroy(error);
if (chunk.next)
return this.rewrite(chunk.next, callback);
callback();
};
this.size += chunk.chunk.length;
this.stream.write(chunk.chunk, chunk.encoding, (error) => {
if (error)
return done(error);
if (this.options.size && this.size >= this.options.size)
return this.rotate(done);
done();
});
};
if (this.stream) {
return this.fsStat(this.filename, (error) => {
if (!error)
return rewrite();
if (error.code !== "ENOENT")
return destroy(error);
this.reclose(() => this.reopen(false, 0, () => rewrite()));
});
}
this.opened = rewrite;
}
init(callback) {
const { immutable, initialRotation, interval, size } = this.options;
if (immutable)
return this.immutate(true, callback);
this.fsStat(this.filename, (error, stats) => {
if (error)
return error.code === "ENOENT" ? this.reopen(false, 0, callback) : callback(error);
if (!stats.isFile())
return callback(new Error(`Can't write on: ${this.filename} (it is not a file)`));
if (initialRotation) {
this.intervalBounds(this.now());
const prev = this.prev;
this.intervalBounds(new Date(stats.mtime.getTime()));
if (prev !== this.prev)
return this.rotate(callback);
}
this.size = stats.size;
if (!size || stats.size < size)
return this.reopen(false, stats.size, callback);
if (interval)
this.intervalBounds(this.now());
this.rotate(callback);
});
}
makePath(name, callback) {
const dir = path_1.parse(name).dir;
this.fsMkdir(dir, (error) => {
if (error) {
if (error.code === "ENOENT")
return this.makePath(dir, (error) => (error ? callback(error) : this.makePath(name, callback)));
return callback(error);
}
callback();
});
}
reopen(retry, size, callback) {
const options = { flags: "a" };
if ("mode" in this.options)
options.mode = this.options.mode;
let called;
const stream = this.fsCreateWriteStream(this.filename, options);
const end = (error) => {
if (called) {
this.error = error;
return;
}
called = true;
this.stream = stream;
if (this.opened) {
process.nextTick(this.opened);
this.opened = null;
}
if (this.destroyer)
process.nextTick(this.destroyer);
callback(error);
};
stream.once("open", () => {
this.size = size;
end();
this.interval();
this.emit("open", this.filename);
});
stream.once("error", (error) => error.code !== "ENOENT" || retry ? end(error) : this.makePath(this.filename, (error) => (error ? end(error) : this.reopen(true, size, callback))));
}
reclose(callback) {
const { stream } = this;
if (!stream)
return callback();
this.stream = null;
stream.once("finish", callback);
stream.end();
}
now() {
return new Date();
}
rotate(callback) {
const { immutable, rotate } = this.options;
this.size = 0;
this.rotation = this.now();
this.clear();
this.reclose(() => (rotate ? this.classical(rotate, callback) : immutable ? this.immutate(false, callback) : this.move(callback)));
this.emit("rotation");
}
findName(tmp, callback, index) {
if (!index)
index = 1;
const { interval, path, intervalBoundary } = this.options;
let filename = `${this.filename}.${index}.rfs.tmp`;
if (index >= 1000)
return callback(new RotatingFileStreamError());
if (!tmp) {
try {
filename = path + this.generator(interval && intervalBoundary ? new Date(this.prev) : this.rotation, index);
}
catch (e) {
return callback(e);
}
}
this.fsStat(filename, error => {
if (!error || error.code !== "ENOENT")
return this.findName(tmp, callback, index + 1);
callback(null, filename);
});
}
move(callback) {
const { compress } = this.options;
let filename;
const open = (error) => {
if (error)
return callback(error);
this.rotated(filename, callback);
};
this.findName(false, (error, found) => {
if (error)
return callback(error);
filename = found;
this.touch(filename, false, (error) => {
if (error)
return callback(error);
if (compress)
return this.compress(filename, open);
this.fsRename(this.filename, filename, open);
});
});
}
touch(filename, retry, callback) {
this.fsOpen(filename, "a", parseInt("666", 8), (error, fd) => {
if (error) {
if (error.code !== "ENOENT" || retry)
return callback(error);
return this.makePath(filename, error => {
if (error)
return callback(error);
this.touch(filename, true, callback);
});
}
return this.fsClose(fd, (error) => {
if (error)
return callback(error);
this.fsUnlink(filename, (error) => {
if (error)
this.emit("warning", error);
callback();
});
});
});
}
classical(count, callback) {
const { compress, path, rotate } = this.options;
let prevName;
let thisName;
if (rotate === count)
delete this.rotatedName;
const open = (error) => {
if (error)
return callback(error);
this.rotated(this.rotatedName, callback);
};
try {
prevName = count === 1 ? this.filename : path + this.generator(count - 1);
thisName = path + this.generator(count);
}
catch (e) {
return callback(e);
}
const next = count === 1 ? open : () => this.classical(count - 1, callback);
const move = () => {
if (count === 1 && compress)
return this.compress(thisName, open);
this.fsRename(prevName, thisName, (error) => {
if (!error)
return next();
if (error.code !== "ENOENT")
return callback(error);
this.makePath(thisName, (error) => {
if (error)
return callback(error);
this.fsRename(prevName, thisName, (error) => (error ? callback(error) : next()));
});
});
};
this.fsStat(prevName, (error) => {
if (error) {
if (error.code !== "ENOENT")
return callback(error);
return next();
}
if (!this.rotatedName)
this.rotatedName = thisName;
move();
});
}
clear() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
return true;
}
intervalBoundsBig(now) {
let year = now.getFullYear();
let month = now.getMonth();
let day = now.getDate();
let hours = now.getHours();
const { num, unit } = this.options.interval;
if (unit === "M") {
day = 1;
hours = 0;
}
else if (unit === "d")
hours = 0;
else
hours = parseInt((hours / num), 10) * num;
this.prev = new Date(year, month, day, hours, 0, 0, 0).getTime();
if (unit === "M")
month += num;
else if (unit === "d")
day += num;
else
hours += num;
this.next = new Date(year, month, day, hours, 0, 0, 0).getTime();
}
intervalBounds(now) {
const unit = this.options.interval.unit;
if (unit === "M" || unit === "d" || unit === "h")
this.intervalBoundsBig(now);
else {
let period = 1000 * this.options.interval.num;
if (unit === "m")
period *= 60;
this.prev = parseInt((now.getTime() / period), 10) * period;
this.next = this.prev + period;
}
return new Date(this.prev);
}
interval() {
if (!this.options.interval)
return;
this.intervalBounds(this.now());
const set = () => {
const time = this.next - this.now().getTime();
this.timer = time > this.maxTimeout ? setTimeout(set, this.maxTimeout) : setTimeout(() => this.rotate(error => (this.error = error)), time);
this.timer.unref();
};
set();
}
compress(filename, callback) {
const { compress } = this.options;
const done = (error) => {
if (error)
return callback(error);
this.fsUnlink(this.filename, callback);
};
if (typeof compress === "function")
this.external(filename, done);
else
this.gzip(filename, done);
}
external(filename, callback) {
const compress = this.options.compress;
let cont;
try {
cont = compress(this.filename, filename);
}
catch (e) {
return callback(e);
}
this.findName(true, (error, found) => {
if (error)
return callback(error);
this.fsOpen(found, "w", parseInt("777", 8), (error, fd) => {
if (error)
return callback(error);
const unlink = (error) => {
this.fsUnlink(found, (error2) => {
if (error2)
this.emit("warning", error2);
callback(error);
});
};
this.fsWrite(fd, cont, "utf8", (error) => {
this.fsClose(fd, (error2) => {
if (error) {
if (error2)
this.emit("warning", error2);
return unlink(error);
}
if (error2)
return unlink(error2);
if (found.indexOf(path_1.sep) === -1)
found = `.${path_1.sep}${found}`;
this.exec(found, unlink);
});
});
});
});
}
gzip(filename, callback) {
const { mode } = this.options;
const options = mode ? { mode } : {};
const inp = this.fsCreateReadStream(this.filename, {});
const out = this.fsCreateWriteStream(filename, options);
const zip = this.createGzip();
[inp, out, zip].map(stream => stream.once("error", callback));
out.once("finish", callback);
inp.pipe(zip).pipe(out);
}
rotated(filename, callback) {
const { maxFiles, maxSize } = this.options;
const open = (error) => {
if (error)
return callback(error);
this.reopen(false, 0, callback);
this.emit("rotated", filename);
};
if (maxFiles || maxSize)
return this.history(filename, open);
open();
}
history(filename, callback) {
let { history } = this.options;
this.fsReadFile(history, "utf8", (error, data) => {
if (error) {
if (error.code !== "ENOENT")
return callback(error);
return this.historyGather([filename], 0, [], callback);
}
const files = data.split("\n");
files.push(filename);
this.historyGather(files, 0, [], callback);
});
}
historyGather(files, index, res, callback) {
if (index === files.length)
return this.historyCheckFiles(res, callback);
this.fsStat(files[index], (error, stats) => {
if (error) {
if (error.code !== "ENOENT")
return callback(error);
}
else if (stats.isFile()) {
res.push({
name: files[index],
size: stats.size,
time: stats.ctime.getTime()
});
}
else
this.emit("warning", new Error(`File '${files[index]}' contained in history is not a regular file`));
this.historyGather(files, index + 1, res, callback);
});
}
historyRemove(files, size, callback) {
const file = files.shift();
this.fsUnlink(file.name, (error) => {
if (error)
return callback(error);
this.emit("removed", file.name, !size);
callback();
});
}
historyCheckFiles(files, callback) {
const { maxFiles } = this.options;
files.sort((a, b) => a.time - b.time);
if (!maxFiles || files.length <= maxFiles)
return this.historyCheckSize(files, callback);
this.historyRemove(files, false, (error) => (error ? callback(error) : this.historyCheckFiles(files, callback)));
}
historyCheckSize(files, callback) {
const { maxSize } = this.options;
let size = 0;
if (!maxSize)
return this.historyWrite(files, callback);
files.map(e => (size += e.size));
if (size <= maxSize)
return this.historyWrite(files, callback);
this.historyRemove(files, true, (error) => (error ? callback(error) : this.historyCheckSize(files, callback)));
}
historyWrite(files, callback) {
this.fsWriteFile(this.options.history, files.map(e => e.name).join("\n") + "\n", "utf8", (error) => {
if (error)
return callback(error);
this.emit("history");
callback();
});
}
immutate(first, callback, index, now) {
if (!index) {
index = 1;
now = this.now();
}
if (index >= 1001)
return callback(new RotatingFileStreamError());
try {
this.filename = this.options.path + this.generator(now, index);
}
catch (e) {
return callback(e);
}
const open = (size, callback) => {
if (first) {
this.last = this.filename;
return this.reopen(false, size, callback);
}
this.rotated(this.last, (error) => {
this.last = this.filename;
callback(error);
});
};
this.fsStat(this.filename, (error, stats) => {
const { size } = this.options;
if (error) {
if (error.code === "ENOENT")
return open(0, callback);
return callback(error);
}
if (!stats.isFile())
return callback(new Error(`Can't write on: '${this.filename}' (it is not a file)`));
if (size && stats.size >= size)
return this.immutate(first, callback, index + 1, now);
open(stats.size, callback);
});
}
}
exports.RotatingFileStream = RotatingFileStream;
function buildNumberCheck(field) {
return (type, options, value) => {
const converted = parseInt(value, 10);
if (type !== "number" || converted !== value || converted <= 0)
throw new Error(`'${field}' option must be a positive integer number`);
};
}
function buildStringCheck(field, check) {
return (type, options, value) => {
if (type !== "string")
throw new Error(`Don't know how to handle 'options.${field}' type: ${type}`);
options[field] = check(value);
};
}
function checkMeasure(value, what, units) {
const ret = {};
ret.num = parseInt(value, 10);
if (isNaN(ret.num))
throw new Error(`Unknown 'options.${what}' format: ${value}`);
if (ret.num <= 0)
throw new Error(`A positive integer number is expected for 'options.${what}'`);
ret.unit = value.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;
}
const intervalUnits = {
M: true,
d: true,
h: true,
m: true,
s: true
};
RotatingFileStream.prototype._rewrite = function() {
const self = this;
const callback = function() {
if(self.ending) self._close(Writable.prototype.end.bind(self));
};
if(this.err) {
const chunks = this.chunks;
this.chunks = [];
chunks.map(e => {
if(e.cb) e.cb();
});
return callback();
}
if(this.writing || this.rotation) return;
if(this.options.size && this.size >= this.options.size) return this.rotate();
if(! this.stream) return;
if(! this.chunks.length) return callback();
const chunk = this.chunks[0];
this.chunks.shift();
this.size += chunk.chunk.length;
this.writing = true;
this.stream.write(chunk.chunk, function(err) {
self.writing = false;
if(err) self.emit("error", err);
if(chunk.cb) chunk.cb();
process.nextTick(self._rewrite.bind(self));
});
function checkIntervalUnit(ret, unit, amount) {
if (parseInt((amount / ret.num), 10) * ret.num !== amount)
throw new Error(`An integer divider of ${amount} is expected as ${unit} for 'options.interval'`);
}
function checkInterval(value) {
const ret = checkMeasure(value, "interval", intervalUnits);
switch (ret.unit) {
case "h":
checkIntervalUnit(ret, "hours", 24);
break;
case "m":
checkIntervalUnit(ret, "minutes", 60);
break;
case "s":
checkIntervalUnit(ret, "seconds", 60);
break;
}
return ret;
}
const sizeUnits = {
B: true,
G: true,
K: true,
M: true
};
RotatingFileStream.prototype._write = function(chunk, encoding, callback) {
this.chunks.push({ chunk: chunk, cb: callback });
this._rewrite();
function checkSize(value) {
const ret = checkMeasure(value, "size", sizeUnits);
if (ret.unit === "K")
return ret.num * 1024;
if (ret.unit === "M")
return ret.num * 1048576;
if (ret.unit === "G")
return ret.num * 1073741824;
return ret.num;
}
const checks = {
compress: (type, options, value) => {
if (!value)
throw new Error("A value for 'options.compress' must be specified");
if (type === "boolean")
return (options.compress = (source, dest) => `cat ${source} | gzip -c9 > ${dest}`);
if (type === "function")
return;
if (type !== "string")
throw new Error(`Don't know how to handle 'options.compress' type: ${type}`);
if (value !== "gzip")
throw new Error(`Don't know how to handle compression method: ${value}`);
},
encoding: (type, options, value) => new util_1.TextDecoder(value),
history: (type) => {
if (type !== "string")
throw new Error(`Don't know how to handle 'options.history' type: ${type}`);
},
immutable: () => { },
initialRotation: () => { },
interval: buildStringCheck("interval", checkInterval),
intervalBoundary: () => { },
maxFiles: buildNumberCheck("maxFiles"),
maxSize: buildStringCheck("maxSize", checkSize),
mode: () => { },
path: (type, options, value) => {
if (type !== "string")
throw new Error(`Don't know how to handle 'options.path' type: ${type}`);
if (value[value.length - 1] !== path_1.sep)
options.path = value + path_1.sep;
},
rotate: buildNumberCheck("rotate"),
size: buildStringCheck("size", checkSize)
};
RotatingFileStream.prototype._writev = function(chunks, callback) {
chunks[chunks.length - 1].cb = callback;
this.chunks = this.chunks.concat(chunks);
this._rewrite();
};
RotatingFileStream.prototype.end = function() {
var args = [];
for(var i = 0; i < arguments.length; ++i) {
if("function" === typeof arguments[i]) {
this.once("finish", arguments[i]);
break;
}
if(i > 1) break;
args.push(arguments[i]);
}
this.ending = true;
if(args.length) this.write.apply(this, args);
else this._rewrite();
};
RotatingFileStream.prototype.firstOpen = function() {
var self = this;
if(this.options.immutable) return this.immutate(true);
try {
this.name = this.generator(null);
}
catch(e) {
return this.emit("error", e);
}
this.once("open", this.interval.bind(this));
fs.stat(this.name, function(err, stats) {
if(err) {
if(err.code === "ENOENT") return self.open();
return self.emit("error", err);
}
if(! stats.isFile()) return self.emit("error", new Error("Can't write on: " + self.name + " (it is not a file)"));
if(self.options.initialRotation) {
var prev;
self._interval(self.now());
prev = self.prev;
self._interval(stats.mtime.getTime());
if(prev !== self.prev) return self.rotate();
}
self.size = stats.size;
if(! self.options.size || stats.size < self.options.size) return self.open();
if(self.options.interval) self._interval(self.now());
self.rotate();
});
};
RotatingFileStream.prototype.immutate = function(first, index, now) {
if(! index) {
index = 1;
now = new Date(this.now());
}
if(index >= 1001) return this.emit("error", this.exhausted());
try {
this.name = this.generator(now, index);
}
catch(e) {
return this.emit("error", e);
}
var open = function(size) {
this.size = size;
this.open();
this.once(
"open",
function() {
if(! first) this.emit("rotated", this.last);
this.last = this.name;
this.interval();
}.bind(this)
);
}.bind(this);
fs.stat(
this.name,
function(err, stats) {
if(err) {
if(err.code === "ENOENT") return open(0);
return this.emit("error", err);
}
if(! stats.isFile()) return this.emit("error", new Error("Can't write on: " + this.name + " (it is not a file)"));
if(this.options.size && stats.size >= this.options.size) return this.immutate(first, index + 1, now);
open(stats.size);
}.bind(this)
);
};
RotatingFileStream.prototype.move = function(retry) {
var name;
var self = this;
var callback = function(err) {
if(err) return self.emit("error", err);
self.open();
if(self.options.compress) self.compress(name);
else {
self.emit("rotated", name);
self.interval();
}
};
this.findName({}, self.options.compress, function(err, found) {
if(err) return callback(err);
name = found;
fs.rename(self.name, name, function(err) {
if(err && err.code !== "ENOENT" && ! retry) return callback(err);
if(! err) return callback();
utils.makePath(name, function(err) {
if(err) return callback(err);
self.move(true);
});
});
});
};
RotatingFileStream.prototype.now = function() {
return Date.now();
};
RotatingFileStream.prototype.open = function(retry) {
var fd;
var self = this;
var options = { flags: "a" };
var callback = function(err) {
if(err) self.emit("error", err);
process.nextTick(self._rewrite.bind(self));
};
if("mode" in this.options) options.mode = this.options.mode;
var stream = fs.createWriteStream(this.name, options);
stream.once("open", function() {
self.stream = stream;
self.emit("open", self.name);
callback();
});
stream.once("error", function(err) {
if(err.code !== "ENOENT" && ! retry) return callback(err);
utils.makePath(self.name, function(err) {
if(err) return callback(err);
self.open(true);
});
});
};
RotatingFileStream.prototype.rotate = function() {
this.size = 0;
this.rotation = new Date();
this.emit("rotation");
this._clear();
this._close(this.options.rotate ? this.classical.bind(this, this.options.rotate) : this.options.immutable ? this.immutate.bind(this) : this.move.bind(this));
};
for(var i in compress) RotatingFileStream.prototype[i] = compress[i];
for(i in interval) RotatingFileStream.prototype[i] = interval[i];
module.exports = RotatingFileStream;
module.exports.default = RotatingFileStream;
function checkOpts(options) {
const ret = {};
for (const opt in options) {
const value = options[opt];
const type = typeof value;
if (!(opt in checks))
throw new Error(`Unknown option: ${opt}`);
ret[opt] = options[opt];
checks[opt](type, ret, value);
}
if (!ret.path)
ret.path = "";
if (!ret.interval) {
delete ret.immutable;
delete ret.initialRotation;
delete ret.intervalBoundary;
}
if (ret.rotate) {
delete ret.history;
delete ret.immutable;
delete ret.maxFiles;
delete ret.maxSize;
delete ret.intervalBoundary;
}
if (ret.immutable)
delete ret.compress;
if (!ret.intervalBoundary)
delete ret.initialRotation;
return ret;
}
function createClassical(filename) {
return (index) => (index ? `${filename}.${index}` : filename);
}
function createGenerator(filename) {
const pad = (num) => (num > 9 ? "" : "0") + num;
return (time, index) => {
if (!time)
return filename;
const month = time.getFullYear() + "" + pad(time.getMonth() + 1);
const day = pad(time.getDate());
const hour = pad(time.getHours());
const minute = pad(time.getMinutes());
return month + day + "-" + hour + minute + "-" + pad(index) + "-" + filename;
};
}
function createStream(filename, options) {
if (typeof options === "undefined")
options = {};
else if (typeof options !== "object")
throw new Error(`The "options" argument must be of type object. Received type ${typeof options}`);
const opts = checkOpts(options);
let generator;
if (typeof filename === "string")
generator = options.rotate ? createClassical(filename) : createGenerator(filename);
else if (typeof filename === "function")
generator = filename;
else
throw new Error(`The "filename" argument must be one of type string or function. Received type ${typeof filename}`);
return new RotatingFileStream(generator, opts);
}
exports.createStream = createStream;
{
"name": "rotating-file-stream",
"version": "1.4.6",
"version": "2.0.0",
"description": "Opens a stream.Writable to a file rotated by interval and/or size. A logrotate alternative.",
"scripts": {
"all": "npm run npmignore && npm run eslint && npm run coverage && npm run ts",
"coverage": "TZ=\"Europe/Rome\" ./node_modules/.bin/nyc -r lcov -r text -r text-summary npm test",
"debug": "node --inspect-brk ./node_modules/.bin/_mocha test",
"eslint": "./node_modules/.bin/eslint *.js test/*js",
"npmignore": "echo '.codeclimate.yml\\n.eslintrc\\n.gitignore\\n.gitattributes\\n.travis.yml\\n.vscode\\nCHANGELOG.md\\nREADME.md\\ntest' > .npmignore ; cat .gitignore >> .npmignore",
"test": "TZ=\"Europe/Rome\" ./node_modules/.bin/_mocha test",
"ts": "node_modules/.bin/tsc index.d.ts --lib es6"
"all": "npm run eslint && npm run coverage",
"clean": "node -r ts-node/register utils.ts clean",
"coverage": "tsc && TZ=\"Europe/Rome\" nyc -r lcov -r text -r text-summary -r html mocha -r ts-node/register test/*ts",
"eslint": "eslint index.ts utils.ts test/*ts",
"ignore": "node -r ts-node/register utils.ts ignore",
"prepare": "npm run ignore && tsc && npm run readme",
"readme": "node -r ts-node/register utils.ts readme",
"test": "npm run clean && mocha -r ts-node/register test/*ts"
},

@@ -22,3 +23,3 @@ "bugs": "https://github.com/iccicci/rotating-file-stream/issues",

"engines": {
"node": ">=6.0"
"node": ">=10.0"
},

@@ -36,11 +37,19 @@ "author": "Daniele Ricci <daniele.icc@gmail.com> (https://github.com/iccicci)",

"license": "MIT",
"funding": {
"url": "https://www.blockchain.com/btc/address/12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN"
},
"readmeFilename": "README.md",
"types": "index.d.ts",
"devDependencies": {
"eslint": "6.5.1",
"@types/mocha": "5.2.7",
"@types/node": "12.12.12",
"@typescript-eslint/eslint-plugin": "2.8.0",
"@typescript-eslint/parser": "2.8.0",
"eslint": "6.7.0",
"mocha": "6.2.2",
"nyc": "14.1.1",
"typescript": "3.6.4",
"@types/node": "12.11.1"
"prettier": "1.19.1",
"ts-node": "8.5.2",
"typescript": "3.7.2"
}
}

@@ -12,8 +12,8 @@ # rotating-file-stream

[![NPM](https://nodei.co/npm/rotating-file-stream.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/rotating-file-stream/)
[![NPM](https://nodei.co/npm/rotating-file-stream.png?downloads=true&downloadRank=true)](https://nodei.co/npm/rotating-file-stream/)
### Description
Creates a [stream.Writable](https://nodejs.org/api/stream.html#stream_class_stream_writable) to a file which is rotated.
Rotation behaviour can be deeply customized; optionally, classical UNIX **logrotate** behaviour can be used.
Creates a [stream.Writable](https://nodejs.org/api/stream.html#stream_class_stream_writable) to a file which is
rotated. Rotation behaviour can be deeply customized; optionally, classical UNIX **logrotate** behaviour can be used.

@@ -23,7 +23,7 @@ ### Usage

```javascript
var rfs = require("rotating-file-stream");
var stream = rfs("file.log", {
size: "10M", // rotate every 10 MegaBytes written
interval: "1d", // rotate daily
compress: "gzip" // compress rotated files
const rfs = require("rotating-file-stream");
const stream = rfs.createStream("file.log", {
size: "10M", // rotate every 10 MegaBytes written
interval: "1d", // rotate daily
compress: "gzip" // compress rotated files
});

@@ -42,83 +42,128 @@ ```

- [Upgrading from v1.x.x to v2.x.x](#upgrading-from-v1xx-to-v2xx)
- [API](#api)
- [rfs.createStream(filename[, options])](#rfscreatestreamfilename-options)
- [filename](#filename)
- [filename(time[, index])](#filenametime-index)
- [filename(index)](#filenameindex)
- [Class: RotatingFileStream](#class-rotatingfilestream)
- [RotatingFileStream(filename, options)](#new-rotatingfilestreamfilename-options)
- [filename](#filename-stringfunction)
- [options](#options-object)
- [compress](#compress)
- [history](#history)
- [immutable](#immutable)
- [initialRotation](#initialrotation)
- [interval](#interval)
- [maxFiles](#maxfiles)
- [maxSize](#maxsize)
- [path](#path)
- [rotate](#rotate)
- [rotationTime](#rotationtime)
- [size](#size)
- [Events](#events)
- [Rotation logic](#rotation-logic)
- [Under the hood](#under-the-hood)
- [Compatibility](#compatibility)
- [TypeScript](#typescript)
- [Licence](#licence)
- [Bugs](#bugs)
- [ChangeLog](#changelog)
- [Donating](#donating)
- [Event: 'history'](#event-history)
- [Event: 'open'](#event-open)
- [Event: 'removed'](#event-removed)
- [Event: 'rotation'](#event-rotation)
- [Event: 'rotated'](#event-rotated)
- [Event: 'warning'](#event-warning)
- [options](#options)
- [compress](#compress)
- [encoding](#encoding)
- [history](#history)
- [immutable](#immutable)
- [initialRotation](#initialrotation)
- [interval](#interval)
- [intervalBoundary](#intervalboundary)
- [maxFiles](#maxfiles)
- [maxSize](#maxsize)
- [mode](#mode)
- [path](#path)
- [rotate](#rotate)
- [size](#size)
- [Rotation logic](#rotation-logic)
- [Under the hood](#under-the-hood)
- [Compatibility](#compatibility)
- [TypeScript](#typescript)
- [Licence](#licence)
- [Bugs](#bugs)
- [ChangeLog](#changelog)
- [Donating](#donating)
# Upgrading from v1.x.x to v2.x.x
There are two main changes in package interface.
In **v1** the _default export_ of the packege was directly the **RotatingFileStream** _constructor_ and the caller
have to use it; while in **v2** there is no _default export_ and the caller should use the
[createStream](#rfscreatestreamfilename-options) exported function and should not directly use
[RotatingFileStream](#class-rotatingfilestream) class.
This is quite easy to discover: if this change is not applied, nothing than a runtime error can happen.
The other important change is the removal of option **rotationTime** and the introduction of **intervalBoundary**.
In **v1** the `time` argument passed to the _filename generator_ function, by default, is the time when _rotaion job_
started, while if [`options.interval`](#interval) option is used, it is the lower boundary of the time interval within
_rotaion job_ started. Later I was asked to add the possibility to restore the default value for this argument so I
introduced `options.rotationTime` option with this purpose. At the end the result was something a bit confusing,
something I never liked.
In **v2** the `time` argument passed to the _filename generator_ function is always the time when _rotaion job_
started, unless [`options.intervalBoundary`](#intervalboundary) option is used. In a few words, to maintain back compatibility
upgrading from **v1** to **v2**, just follow this rules:
- using [`options.rotation`](#rotation): nothing to do
- not using [`options.rotation`](#rotation):
- not using [`options.interval`](#interval): nothing to do
- using [`options.interval`](#interval):
- using `options.rotationTime`: to remove it
- not using `options.rotationTime`: then use [`options.intervalBoundary`](#intervalboundary).
# API
```javascript
require("rotating-file-stream");
const rfs = require("rotating-file-stream");
```
Returns **RotatingFileStream** constructor.
## rfs.createStream(filename[, options])
## Class: RotatingFileStream
- `filename` [&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) |
[&lt;Function>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) The name
of the file or the function to generate it, called _file name generator_. See below for
[details](#filename-stringfunction).
- `options` [&lt;Object>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)
Rotation options, See below for [details](#options).
- Returns: [&lt;RotatingFileStream>](#class-rotatingfilestream) The **rotating file stream**!
Extends [stream.Writable](https://nodejs.org/api/stream.html#stream_class_stream_writable).
This interface is inspired to
[fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options) one. The file is rotated
following _options_ rules.
## [new] RotatingFileStream(filename, options)
### filename
Returns a new **RotatingFileStream** to _filename_ as
[fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options) does.
The file is rotated following _options_ rules.
### filename {String|Function}
The most complex problem about file name is: "how to call the rotated file name?"
The answer to this question may vary in many forms depending on application requirements and/or specifications.
If there are no requirements, a _String_ can be used and _default rotated file name generator_ will be used;
otherwise a _Function_ which returns the _rotated file name_ can be used.
If there are no requirements, a `string` can be used and _default rotated file name generator_ will be used;
otherwise a `Function` which returns the _rotated file name_ can be used.
#### function filename(time, index)
**Note:**
if part of returned destination path does not exists, the rotation job will try to create it.
- time: {Date} If both rotation by interval is enabled and **options.rotationTime** [(see below)](#rotationtime) is
**false**, 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.
#### filename(time[, index])
- `time` [&lt;Date>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
- By default: the time when rotation job started;
- if both [`options.interval`](#interval) and [`intervalBoundary`](#intervalboundary) options are enabled: the start
time of rotation period.
If `null`, the _not-rotated file name_ must be returned.
- `index` [&lt;number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) The
progressive index of rotation by size in the same rotation period.
An example of a complex _rotated file name generator_ function could be:
```javascript
function pad(num) {
return (num > 9 ? "" : "0") + num;
}
const pad = num => (num > 9 ? "" : "0") + num;
const generator = (time, index) => {
if (!time) return "file.log";
function generator(time, index) {
if (!time) return "file.log";
var month = time.getFullYear() + "" + pad(time.getMonth() + 1);
var day = pad(time.getDate());
var hour = pad(time.getHours());
var minute = pad(time.getMinutes());
var month = time.getFullYear() + "" + pad(time.getMonth() + 1);
var day = pad(time.getDate());
var hour = pad(time.getHours());
var minute = pad(time.getMinutes());
return `${month}/${month}${day}-${hour}${minute}-${index}-file.log`;
};
return month + "/" + month + day + "-" + hour + minute + "-" + index + "-file.log";
}
var rfs = require("rotating-file-stream");
var stream = rfs(generator, {
size: "10M",
interval: "30m"
const rfs = require("rotating-file-stream");
const stream = rfs(generator, {
size: "10M",
interval: "30m"
});

@@ -128,43 +173,117 @@ ```

**Note:**
if both rotation by interval and rotation by time are used, returned _rotated file name_ **must** be function of both
parameters _time_ and _index_. Alternatively, **rotationTime** _option_ can be used (to see below).
if all of [`options.interval`](#interval), [`options.size`](#size) and [`options.intervalBoundary`](#intervalBoundary)
are used, returned _rotated file name_ **must** be function of both arguments `time` and `index`.
If classical **logrotate** behaviour is enabled _rotated file name_ is only a function of _index_.
#### filename(index)
#### function filename(index)
- `index` [&lt;number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) The
progressive index of rotation. If `null`, the _not-rotated file name_ must be returned.
- index {Number} The progressive index of rotation. If **null**, the _not-rotated file name_ must be returned.
If classical **logrotate** behaviour is enabled (by [`options.rotate`](#rotate)), _rotated file name_ is only a
function of `index`.
**Note:**
The _not-rotated file name_ **must** be only the _filename_, to specify a _path_ the appropriate option **must** be used.
## Class: RotatingFileStream
```javascript
rfs("path/to/file.log"); // wrong
rfs("file.log", { path: "path/to" }); // OK
```
Extends [stream.Writable](https://nodejs.org/api/stream.html#stream_class_stream_writable). It should not be directly
used. Exported only to be used with `instanceof` operator and similar.
**Note:**
if part of returned destination path does not exists, the rotation job will try to create it.
### Event: 'history'
### options {Object}
The `history` event is emitted once the _history check job _ is completed.
- compress: {String|Function|True} (default: null) Specifies compression method of rotated files.
- highWaterMark: {Number} (default: null) Proxied to [new stream.Writable](https://nodejs.org/api/stream.html#stream_constructor_new_stream_writable_options)
- history: {String} (default: null) Specifies the _history filename_.
- immutable: {Boolean} (default: null) Never mutates file names.
- initialRotation: {Boolean} (default: null) Initial rotation based on _not-rotated file_ timestamp.
- interval: {String} (default: null) Specifies the time interval to rotate the file.
- maxFiles: {Integer} (default: null) Specifies the maximum number of rotated files to keep.
- maxSize: {String} (default: null) Specifies the maximum size of rotated files to keep.
- mode: {Integer} (default: null) Proxied to [fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options)
- path: {String} (default: null) Specifies the base path for files.
- rotate: {Integer} (default: null) Enables the classical UNIX **logrotate** behaviour.
- rotationTime: {Boolean} (default: null) Makes rotated file name with time of rotation.
- size: {String} (default: null) Specifies the file size to rotate the file.
### Event: 'open'
#### path
- `filename` [&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) Is
constant unless [`options.immutable`](#immutable) is `true`.
The `open` event is emitted once the _not-rotated file_ is opened.
### Event: 'removed'
- `filename` [&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The
name of the removed file.
- `number` [&lt;boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type)
- `true` if the file was removed due to [`options.maxFiles`](#maxFiles)
- `false` if the file was removed due to [`options.maxSize`](#maxSize)
The `removed` event is emitted once a _rotated file_ is removed due to [`options.maxFiles`](#maxFiles) or
[`options.maxSize`](#maxSize).
### Event: 'rotation'
The `rotation` event is emitted once the _rotation job_ is started.
### Event: 'rotated'
- `filename` [&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The
_rotated file name_ produced.
The `rotated` event is emitted once the _rotation job_ is completed.
### Event: 'warning'
- `error` [&lt;Error>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) The
non blocking error.
The `warning` event is emitted once a non blocking error happens.
## options
- [`compress`](#compress):
[&lt;boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) |
[&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) |
[&lt;Function>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function)
Specifies compression method of rotated files. **Default:** `null`.
- [`encoding`](#encoding):
[&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type)
Specifies the default encoding. **Default:** `'utf8'`.
- [`history`](#history):
[&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type)
Specifies the _history filename_. **Default:** `null`.
- [`immutable`](#immutable):
[&lt;boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type)
Never mutate file names. **Default:** `null`.
- [`initialRotation`](#initialRotation):
[&lt;boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type)
Initial rotation based on _not-rotated file_ timestamp. **Default:** `null`.
- [`interval`](#interval):
[&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type)
Specifies the time interval to rotate the file. **Default:** `null`.
- [`intervalBoundary`](#intervalBoundary):
[&lt;boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type)
Makes rotated file name with lower boundary of rotation period. **Default:** `null`.
- [`maxFiles`](#maxFiles):
[&lt;number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type)
Specifies the maximum number of rotated files to keep. **Default:** `null`.
- [`maxSize`](#maxSize):
[&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type)
Specifies the maximum size of rotated files to keep. **Default:** `null`.
- [`mode`](#mode):
[&lt;number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type)
Proxied to [fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options).
**Default:** `0o666`.
- [`path`](#path):
[&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type)
Specifies the base path for files. **Default:** `null`.
- [`rotate`](#rotate):
[&lt;number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type)
Enables the classical UNIX **logrotate** behaviour. **Default:** `null`.
- [`size`](#size):
[&lt;string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type)
Specifies the file size to rotate the file. **Default:** `null`.
### encoding
Specifies the default encoding that is used when no encoding is specified as an argument to
[stream.write()](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback).
### mode
Proxied to [fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options).
### path
If present, it is prepended to generated file names as well as for history file.
#### size
### size

@@ -195,3 +314,3 @@ Accepts a positive integer followed by one of these possible letters:

#### interval
### interval

@@ -227,15 +346,27 @@ Accepts a positive integer followed by one of these possible letters:

#### compress
### intervalBoundary
Due the nature of **Node.js** compression may be done with an external command (to use other CPUs than the one used
by **Node.js**) or with internal code (to use the CPU used by **Node.js**). This decision is left to you.
If set to `true`, the argument `time` of _filename generator_ is no longer the time when _rotation job_ started, but
the _lower boundary_ of rotation interval.
Following fixed strings are allowed to compress the files with internal libraries:
**Note:**
this option has effec only if [`options.interval`](#interval) is used.
- bzip2 (**not implemented yet**)
- gzip
### initialRotation
To enable external compression, a _function_ can be used or simply the _boolean_ **true** value to use default
When program stops in a rotation period then restarts in a new rotation period, logs of different rotation period will
go in the next rotated file; in a few words: a rotation job is lost. If this option is set to `true` an initial check
is performed against the _not-rotated file_ timestamp and, if it falls in a previous rotation period, an initial
rotation job is done as well.
**Note:**
this option has effec only if [`options.intervalBoundary`](#intervalboundary) is used.
### compress
For historical reasons external compression can be used, but the best choice is to use the value `"gzip"`.
To enable external compression, a _function_ can be used or simply the _boolean_ `true` value to use default
external compression.
The function should accept _source_ and _dest_ file names and must return the shell command to be executed to
The function should accept `source` and `dest` file names and must return the shell command to be executed to
compress the file.

@@ -247,4 +378,4 @@ The two following code snippets have exactly the same effect:

var stream = rfs("file.log", {
size: "10M",
compress: true
size: "10M",
compress: true
});

@@ -256,6 +387,4 @@ ```

var stream = rfs("file.log", {
size: "10M",
compress: function(source, dest) {
return "cat " + source + " | gzip -c9 > " + dest;
}
size: "10M",
compress: (source, dest) => "cat " + source + " | gzip -c9 > " + dest
});

@@ -265,3 +394,3 @@ ```

**Note:**
this option is ignored if **immutable** is set to **true**.
this option is ignored if [`options.immutable`](#immutable) is used.

@@ -272,6 +401,6 @@ **Note:**

#### initialRotation
### initialRotation
When program stops in a rotation period then restarts in a new rotation period, logs of different rotation period will
go in the next rotated file; in a few words: a rotation job is lost. If this option is set to **true** an initial check
go in the next rotated file; in a few words: a rotation job is lost. If this option is set to `true` an initial check
is performed against the _not-rotated file_ timestamp and, if it falls in a previous rotation period, an initial

@@ -281,6 +410,10 @@ rotation job is done as well.

**Note:**
this option is ignored if **rotationTime** is set to **true**.
this option has effect only if both [`options.interval`](#interval) and [`options.intervalBoundary`](#intervalboundary)
are used.
#### rotate
**Note:**
this option is ignored if [`options.rotate`](#rotate) is used.
### rotate
If specified, classical UNIX **logrotate** behaviour is enabled and the value of this option has same effect in

@@ -290,10 +423,13 @@ _logrotate.conf_ file.

**Note:**
following options are ignored if **rotate** option is specified.
if this optoin is used following ones take no effect: [`options.history`](#history), [`options.immutable`](#immutable),
[`options.initialRotation`](#initialrotation), [`options.intervalBoundary`](#intervalboundary),
[`options.maxFiles`](#maxfiles), [`options.maxSize`](#maxsize).
#### immutable
### immutable
If set to **true**, names of generated files never changes. New files are immediately generated with their rotated
name. In other words the _rotated file name generator_ is never called with a **null** _time_ parameter unless to
determinate the _history file_ name; this can happen if **maxFiles** or **maxSize** are used without **history**
option. **rotation** _event_ now has a _filename_ parameter with the newly created file name.
If set to `true`, names of generated files never changes. New files are immediately generated with their rotated
name. In other words the _rotated file name generator_ is never called with a `null` _time_ argument unless to
determinate the _history file_ name; this can happen if [`options.history`](#history) is not used while
[`options.maxFiles`](#maxfiles) or [`options.maxSize`](#maxsize) are used.
The `filename` argument passed to [`'open'`](#event-open) _event_ evaluates now as the newly created file name.

@@ -303,71 +439,26 @@ Useful to send logs to logstash through filebeat.

**Note:**
if this option is set to **true**, **compress** is ignored.
if this option is used, [`options.compress`](#compress) is ignored.
**Note:**
this option is ignored if **interval** is not set.
this option is ignored if [`options.interval`](#interval) is not used.
#### rotationTime
### history
As specified above, if rotation by interval is enabled, the parameter _time_ passed to _rotated file name generator_ is the
start time of rotation period. Setting this option to **true**, parameter _time_ passed is time when rotation job
started.
**Note:**
if this option is set to **true**, **initialRotation** is ignored.
#### history
Due to the complexity that _rotated file names_ can have because of the _filename generator function_, if number or
size of rotated files should not exceed a given limit, the package needs a file where to store this information. This
option specifies the name _history file_. This option takes effect only if at least one of **maxFiles** or **maxSize**
is used. If **null**, the _not rotated filename_ with the '.txt' suffix is used.
option specifies the name _history file_. This option takes effect only if at least one of
[`options.maxFiles`](#maxfiles) or [`options.maxSize`](#maxsize) is used. If `null`, the _not rotated filename_ with
the `'.txt'` suffix is used.
#### maxFiles
### maxFiles
If specified, it's value is the maximum number of _rotated files_ to be kept.
#### maxSize
### maxSize
If specified, it's value must respect same syntax of [size](#size) option and is the maximum size of _rotated files_
to be kept.
If specified, it's value must respect same syntax of [option.size](#size) and is the maximum size of _rotated files_ to
be kept.
## Events
# Rotation logic
Custom _Events_ are emitted by the stream.
```javascript
var rfs = require('rotating-file-stream');
var stream = rfs(...);
stream.on('error', function(err) {
// here are reported blocking errors
// once this event is emitted, the stream will be closed as well
});
stream.on('open', function(filename) {
// no rotated file is open (emitted after each rotation as well)
// filename: useful if immutable option is true
});
stream.on('removed', function(filename, number) {
// rotation job removed the specified old rotated file
// number == true, the file was removed to not exceed maxFiles
// number == false, the file was removed to not exceed maxSize
});
stream.on('rotation', function() {
// rotation job started
});
stream.on('rotated', function(filename) {
// rotation job completed with success producing given filename
});
stream.on('warning', function(err) {
// here are reported non blocking errors
});
```
## Rotation logic
Regardless of when and why rotation happens, the content of a single

@@ -377,3 +468,3 @@ [stream.write](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback)

### by size
## by size

@@ -385,3 +476,3 @@ Once the _not-rotated_ file is opened first time, its size is checked and if it is greater or equal to

### by interval
## by interval

@@ -391,3 +482,3 @@ The package sets a [Timeout](https://nodejs.org/api/timers.html#timers_settimeout_callback_delay_args)

## Under the hood
# Under the hood

@@ -402,43 +493,71 @@ Logs should be handled so carefully, so this package tries to never overwrite files.

performed before going on. This is repeated until a not existing destination file name is found or the
package is exhausted. For this reason the _rotated file name generator_ function may be called several
package is exhausted. For this reason the _rotated file name generator_ function could be called several
times for each rotation job.
If requested by **maxFiles** or **maxSize** options, at the end of a rotation job, a check is performed to ensure that
given limits are respected. This means that **while rotation job is running both the limits could be not respected**,
the same can happen (if **maxFiles** or **maxSize** are changed) till the end of first _rotation job_.
The first check performed is the one against **maxFiles**, in case some files are removed, then the check against
**maxSize** is performed, finally other files can be removed. When **maxFiles** or **maxSize** are enabled for first
time, an _history file_ can be created with one _rotated filename_ (as returned by _filename generator function_) at
each line.
If requested through [`options.maxFiles`](#maxfiles) or [`options.maxSize`](#maxsize), at the end of a rotation job, a
check is performed to ensure that given limits are respected. This means that
**while rotation job is running both the limits could be not respected**. The same can happen till the end of first
rotation job* if [`options.maxFiles`](#maxfiles) or [`options.maxSize`](#maxsize) are changed between two runs.
The first check performed is the one against [`options.maxFiles`](#maxfiles), in case some files are removed, then the
check against [`options.maxSize`](#maxsize) is performed, finally other files can be removed. When
[`options.maxFiles`](#maxfiles) or [`options.maxSize`](#maxsize) are enabled for first time, an \_history file* can be
created with one _rotated filename_ (as returned by _filename generator function_) at each line.
Once an **error** _event_ is emitted, nothing more can be done: the stream is closed as well.
## Compatibility
# Compatibility
Requires **Node.js v10.x**.
The package is tested under [all Node.js versions](https://travis-ci.org/iccicci/rotating-file-stream)
currently supported accordingly to [Node.js Release](https://github.com/nodejs/Release).
## TypeScript
To work with the package under Windows, be sure to configure `bash.exe` as your _script-shell_.
To import the package in a **TypeScript** project, use following import statement.
```
> npm config set script-shell bash.exe
```
# TypeScript
Exported in **TypeScript**.
```typescript
import rfs from "rotating-file-stream";
import { Writable } from "stream";
export declare type Compressor = (source: string, dest: string) => string;
export declare type Generator = (time: number | Date, index?: number) => string;
export interface Options {
compress?: boolean | string | Compressor;
encoding?: string;
history?: string;
immutable?: boolean;
initialRotation?: boolean;
interval?: string;
intervalBoundary?: boolean;
maxFiles?: number;
maxSize?: string;
mode?: number;
path?: string;
rotate?: number;
size?: string;
}
export declare class RotatingFileStream extends Writable {}
export declare function createStream(filename: string | Generator, options?: Options): RotatingFileStream;
```
## Licence
# Licence
[MIT Licence](https://github.com/iccicci/rotating-file-stream/blob/master/LICENSE)
## Bugs
# Bugs
Do not hesitate to report any bug or inconsistency [@github](https://github.com/iccicci/rotating-file-stream/issues).
## ChangeLog
# ChangeLog
[ChangeLog](https://github.com/iccicci/rotating-file-stream/blob/master/CHANGELOG.md)
## Donating
# Donating
If you find useful this package, please consider the opportunity to donate some satoshis to this bitcoin address:
**12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN**
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc