rotating-file-stream
Advanced tools
Comparing version 1.4.6 to 2.0.0
@@ -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 |
126
index.d.ts
@@ -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" | ||
} | ||
} |
509
README.md
@@ -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` [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) | | ||
[<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` [<Object>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) | ||
Rotation options, See below for [details](#options). | ||
- Returns: [<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` [<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` [<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` [<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` [<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` [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The | ||
name of the removed file. | ||
- `number` [<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` [<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` [<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): | ||
[<boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) | | ||
[<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) | | ||
[<Function>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) | ||
Specifies compression method of rotated files. **Default:** `null`. | ||
- [`encoding`](#encoding): | ||
[<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) | ||
Specifies the default encoding. **Default:** `'utf8'`. | ||
- [`history`](#history): | ||
[<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) | ||
Specifies the _history filename_. **Default:** `null`. | ||
- [`immutable`](#immutable): | ||
[<boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) | ||
Never mutate file names. **Default:** `null`. | ||
- [`initialRotation`](#initialRotation): | ||
[<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): | ||
[<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): | ||
[<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): | ||
[<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): | ||
[<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): | ||
[<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): | ||
[<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) | ||
Specifies the base path for files. **Default:** `null`. | ||
- [`rotate`](#rotate): | ||
[<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) | ||
Enables the classical UNIX **logrotate** behaviour. **Default:** `null`. | ||
- [`size`](#size): | ||
[<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** |
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
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
57691
801
0
547
2
10
6
2