rotating-file-stream
Advanced tools
Comparing version 2.1.6 to 3.0.0
@@ -0,1 +1,4 @@ | ||
- 2021-11-04 - v3.0.0 | ||
- Released v3 - please check the [README.md](https://www.npmjs.com/package/rotating-file-stream#upgrading-from-v2-to-v3) | ||
- devDependencies update | ||
- 2021-09-26 - v2.1.6 | ||
@@ -2,0 +5,0 @@ - Made the [package compliant with Node.js v14 and v16](https://github.com/iccicci/rotating-file-stream/issues/63) |
/// <reference types="node" /> | ||
import { Writable } from "stream"; | ||
import { Readable, Writable } from "stream"; | ||
export declare type Compressor = (source: string, dest: string) => string; | ||
export declare type Generator = (time: number | Date, index?: number) => string; | ||
interface RotatingFileStreamEvents { | ||
close: () => void; | ||
drain: () => void; | ||
error: (err: Error) => void; | ||
finish: () => void; | ||
pipe: (src: Readable) => void; | ||
unpipe: (src: Readable) => void; | ||
external: (stdout: string, stderr: string) => void; | ||
history: () => void; | ||
open: (filename: string) => void; | ||
removed: (filename: string, number: boolean) => void; | ||
rotation: () => void; | ||
rotated: (filename: string) => void; | ||
warning: (error: Error) => void; | ||
} | ||
export declare interface RotatingFileStream extends Writable { | ||
addListener<Event extends keyof RotatingFileStreamEvents>(event: Event, listener: RotatingFileStreamEvents[Event]): this; | ||
emit<Event extends keyof RotatingFileStreamEvents>(event: Event, ...args: Parameters<RotatingFileStreamEvents[Event]>): boolean; | ||
on<Event extends keyof RotatingFileStreamEvents>(event: Event, listener: RotatingFileStreamEvents[Event]): this; | ||
once<Event extends keyof RotatingFileStreamEvents>(event: Event, listener: RotatingFileStreamEvents[Event]): this; | ||
prependListener<Event extends keyof RotatingFileStreamEvents>(event: Event, listener: RotatingFileStreamEvents[Event]): this; | ||
prependOnceListener<Event extends keyof RotatingFileStreamEvents>(event: Event, listener: RotatingFileStreamEvents[Event]): this; | ||
removeListener<Event extends keyof RotatingFileStreamEvents>(event: Event, listener: RotatingFileStreamEvents[Event]): this; | ||
} | ||
export interface Options { | ||
@@ -16,2 +40,3 @@ compress?: boolean | string | Compressor; | ||
mode?: number; | ||
omitExtension?: boolean; | ||
path?: string; | ||
@@ -36,2 +61,3 @@ rotate?: number; | ||
mode?: number; | ||
omitExtension?: boolean; | ||
path?: string; | ||
@@ -49,30 +75,23 @@ rotate?: number; | ||
private createGzip; | ||
private destroyer; | ||
private error; | ||
private exec; | ||
private file; | ||
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 initPromise; | ||
private last; | ||
private maxTimeout; | ||
private next; | ||
private opened; | ||
private options; | ||
private prev; | ||
private rotatedName; | ||
private rotation; | ||
private size; | ||
private stream; | ||
private timer; | ||
private stdout; | ||
private timeout; | ||
private timeoutPromise; | ||
constructor(generator: Generator, options: Opts); | ||
@@ -83,4 +102,4 @@ _destroy(error: Error, callback: Callback): void; | ||
_writev(chunks: Chunk[], callback: Callback): void; | ||
private refinal; | ||
private rewrite; | ||
private writeToStdOut; | ||
private init; | ||
@@ -101,11 +120,5 @@ private makePath; | ||
private compress; | ||
private external; | ||
private gzip; | ||
private rotated; | ||
private history; | ||
private historyGather; | ||
private historyRemove; | ||
private historyCheckFiles; | ||
private historyCheckSize; | ||
private historyWrite; | ||
private immutate; | ||
@@ -112,0 +125,0 @@ } |
723
index.js
@@ -8,5 +8,8 @@ "use strict"; | ||
const fs_1 = require("fs"); | ||
const promises_1 = require("fs/promises"); | ||
const path_1 = require("path"); | ||
const util_1 = require("util"); | ||
const timers_1 = require("timers"); | ||
async function exists(filename) { | ||
return new Promise(resolve => (0, fs_1.access)(filename, fs_1.constants.F_OK, error => resolve(!error))); | ||
} | ||
class RotatingFileStreamError extends Error { | ||
@@ -25,16 +28,11 @@ constructor() { | ||
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.fsOpen = promises_1.open; | ||
this.fsReadFile = promises_1.readFile; | ||
this.fsStat = promises_1.stat; | ||
this.generator = generator; | ||
this.maxTimeout = 2147483640; | ||
this.options = options; | ||
this.stdout = process.stdout; | ||
if (maxFiles || maxSize) | ||
@@ -44,25 +42,19 @@ options.history = path + (history ? history : this.generator(null) + ".txt"); | ||
this.on("finish", () => (this.finished = this.clear())); | ||
process.nextTick(() => this.init(error => { | ||
this.error = error; | ||
if (this.opened) | ||
this.opened(); | ||
else if (this.error) | ||
this.emit("error", error); | ||
})); | ||
// In v15 was introduced the _constructor method to delay any _write(), _final() and _destroy() calls | ||
// Untill v16 will be not deprecated we still need this.initPromise | ||
// https://nodejs.org/api/stream.html#stream_writable_construct_callback | ||
(async () => { | ||
try { | ||
this.initPromise = this.init(); | ||
await this.initPromise; | ||
delete this.initPromise; | ||
} | ||
catch (e) { } | ||
})(); | ||
} | ||
_destroy(error, callback) { | ||
const destroyer = () => { | ||
this.clear(); | ||
this.reclose(() => { }); | ||
}; | ||
if (this.stream) | ||
destroyer(); | ||
else | ||
this.destroyer = destroyer; | ||
callback(error); | ||
this.refinal(error, callback); | ||
} | ||
_final(callback) { | ||
if (this.stream) | ||
return this.stream.end(callback); | ||
callback(); | ||
this.refinal(undefined, callback); | ||
} | ||
@@ -75,121 +67,95 @@ _write(chunk, encoding, callback) { | ||
} | ||
rewrite(chunks, index, callback) { | ||
const destroy = (error) => { | ||
this.destroy(error); | ||
return callback(); | ||
}; | ||
const rewrite = () => { | ||
if (this.destroyed) | ||
return callback(this.error); | ||
if (this.error) | ||
return destroy(this.error); | ||
if (!this.stream) { | ||
this.opened = rewrite; | ||
return; | ||
async refinal(error, callback) { | ||
try { | ||
this.clear(); | ||
if (this.initPromise) | ||
await this.initPromise; | ||
if (this.timeoutPromise) | ||
await this.timeoutPromise; | ||
await this.reclose(); | ||
} | ||
catch (e) { | ||
return callback(error || e); | ||
} | ||
callback(error); | ||
} | ||
async rewrite(chunks, index, callback) { | ||
const { size, teeToStdout } = this.options; | ||
try { | ||
if (this.initPromise) | ||
await this.initPromise; | ||
if (this.timeoutPromise) | ||
await this.timeoutPromise; | ||
for (let i = 0; i < chunks.length; ++i) { | ||
const { chunk } = chunks[i]; | ||
this.size += chunk.length; | ||
await this.file.write(chunk); | ||
if (teeToStdout && !this.stdout.destroyed) | ||
this.stdout.write(chunk); | ||
if (size && this.size >= size) | ||
await this.rotate(); | ||
} | ||
const done = (error) => { | ||
if (error) | ||
return destroy(error); | ||
if (++index !== chunks.length) | ||
return this.rewrite(chunks, index, callback); | ||
callback(); | ||
}; | ||
this.size += chunks[index].chunk.length; | ||
this.stream.write(chunks[index].chunk, chunks[index].encoding, (error) => { | ||
if (error) | ||
return done(error); | ||
if (this.options.size && this.size >= this.options.size) | ||
return this.rotate(done); | ||
done(); | ||
}); | ||
if (this.options.teeToStdout && !process.stdout.destroyed) | ||
this.writeToStdOut(chunks[index].chunk, chunks[index].encoding); | ||
}; | ||
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; | ||
catch (e) { | ||
return callback(e); | ||
} | ||
callback(); | ||
} | ||
writeToStdOut(buffer, encoding) { | ||
process.stdout.write(buffer, encoding); | ||
} | ||
init(callback) { | ||
async init() { | ||
const { immutable, initialRotation, interval, size } = this.options; | ||
// In v15 was introduced the _constructor method to delay any _write(), _final() and _destroy() calls | ||
// Once v16 will be deprecated we can restore only following line | ||
// if(immutable) return this.immutate(true); | ||
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); | ||
}); | ||
return new Promise((resolve, reject) => process.nextTick(() => this.immutate(true).then(resolve).catch(reject))); | ||
let stats; | ||
try { | ||
stats = await (0, promises_1.stat)(this.filename); | ||
} | ||
catch (e) { | ||
if (e.code !== "ENOENT") | ||
throw e; | ||
return this.reopen(0); | ||
} | ||
if (!stats.isFile()) | ||
throw 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(); | ||
} | ||
this.size = stats.size; | ||
if (!size || stats.size < size) | ||
return this.reopen(stats.size); | ||
if (interval) | ||
this.intervalBounds(this.now()); | ||
return this.rotate(); | ||
} | ||
makePath(name, callback) { | ||
const dir = (0, 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))); | ||
if (error.code === "EEXIST") | ||
return callback(); | ||
return callback(error); | ||
} | ||
callback(); | ||
}); | ||
async makePath(name) { | ||
return (0, promises_1.mkdir)(name.split(path_1.sep).slice(0, -1).join(path_1.sep), { recursive: true }); | ||
} | ||
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)))); | ||
async reopen(size) { | ||
let file; | ||
try { | ||
file = await (0, promises_1.open)(this.filename, "a", this.options.mode); | ||
} | ||
catch (e) { | ||
if (e.code !== "ENOENT") | ||
throw e; | ||
await this.makePath(this.filename); | ||
file = await (0, promises_1.open)(this.filename, "a", this.options.mode); | ||
} | ||
this.file = file; | ||
this.size = size; | ||
this.interval(); | ||
this.emit("open", this.filename); | ||
} | ||
reclose(callback) { | ||
const { stream } = this; | ||
if (!stream) | ||
return callback(); | ||
this.stream = null; | ||
stream.once("finish", callback); | ||
stream.end(); | ||
async reclose() { | ||
const { file } = this; | ||
if (!file) | ||
return; | ||
delete this.file; | ||
return file.close(); | ||
} | ||
@@ -199,3 +165,3 @@ now() { | ||
} | ||
rotate(callback) { | ||
async rotate() { | ||
const { immutable, rotate } = this.options; | ||
@@ -205,118 +171,73 @@ this.size = 0; | ||
this.clear(); | ||
this.reclose(() => (rotate ? this.classical(rotate, callback) : immutable ? this.immutate(false, callback) : this.move(callback))); | ||
this.emit("rotation"); | ||
await this.reclose(); | ||
if (rotate) | ||
return this.classical(); | ||
if (immutable) | ||
return this.immutate(false); | ||
return this.move(); | ||
} | ||
findName(tmp, callback, index) { | ||
if (!index) | ||
index = 1; | ||
async findName() { | ||
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); | ||
} | ||
for (let index = 1; index < 1000; ++index) { | ||
const filename = path + this.generator(interval && intervalBoundary ? new Date(this.prev) : this.rotation, index); | ||
if (!(await exists(filename))) | ||
return filename; | ||
} | ||
this.fsStat(filename, error => { | ||
if (!error || error.code !== "ENOENT") | ||
return this.findName(tmp, callback, index + 1); | ||
callback(null, filename); | ||
}); | ||
throw new RotatingFileStreamError(); | ||
} | ||
move(callback) { | ||
async move() { | ||
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); | ||
}); | ||
}); | ||
const filename = await this.findName(); | ||
await this.touch(filename); | ||
if (compress) | ||
await this.compress(filename); | ||
else | ||
await (0, promises_1.rename)(this.filename, filename); | ||
return this.rotated(filename); | ||
} | ||
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); | ||
}; | ||
async touch(filename) { | ||
let file; | ||
try { | ||
prevName = count === 1 ? this.filename : path + this.generator(count - 1); | ||
thisName = path + this.generator(count); | ||
file = await this.fsOpen(filename, "a"); | ||
} | ||
catch (e) { | ||
return callback(e); | ||
if (e.code !== "ENOENT") | ||
throw e; | ||
await this.makePath(filename); | ||
file = await (0, promises_1.open)(filename, "a"); | ||
} | ||
const next = count === 1 ? open : () => this.classical(count - 1, callback); | ||
const move = () => { | ||
await file.close(); | ||
return (0, promises_1.unlink)(filename); | ||
} | ||
async classical() { | ||
const { compress, path, rotate } = this.options; | ||
let rotatedName = ""; | ||
for (let count = rotate; count > 0; --count) { | ||
const currName = path + this.generator(count); | ||
const prevName = count === 1 ? this.filename : path + this.generator(count - 1); | ||
if (!(await exists(prevName))) | ||
continue; | ||
if (!rotatedName) | ||
rotatedName = currName; | ||
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(); | ||
await this.compress(currName); | ||
else { | ||
try { | ||
await (0, promises_1.rename)(prevName, currName); | ||
} | ||
catch (e) { | ||
if (e.code !== "ENOENT") | ||
throw e; | ||
await this.makePath(currName); | ||
await (0, promises_1.rename)(prevName, currName); | ||
} | ||
} | ||
if (!this.rotatedName) | ||
this.rotatedName = thisName; | ||
move(); | ||
}); | ||
} | ||
return this.rotated(rotatedName); | ||
} | ||
clear() { | ||
if (this.timer) { | ||
clearTimeout(this.timer); | ||
this.timer = null; | ||
if (this.timeout) { | ||
clearTimeout(this.timeout); | ||
this.timeout = null; | ||
} | ||
@@ -365,63 +286,36 @@ return true; | ||
this.intervalBounds(this.now()); | ||
const set = () => { | ||
const set = async () => { | ||
const time = this.next - this.now().getTime(); | ||
if (time <= 0) | ||
return this.rotate(error => (this.error = error)); | ||
this.timer = (0, timers_1.setTimeout)(set, time > this.maxTimeout ? this.maxTimeout : time); | ||
this.timer.unref(); | ||
if (time <= 0) { | ||
try { | ||
this.timeoutPromise = this.rotate(); | ||
await this.timeoutPromise; | ||
delete this.timeoutPromise; | ||
} | ||
catch (e) { } | ||
} | ||
else { | ||
this.timeout = setTimeout(set, time > this.maxTimeout ? this.maxTimeout : time); | ||
this.timeout.unref(); | ||
} | ||
}; | ||
set(); | ||
} | ||
compress(filename, callback) { | ||
async compress(filename) { | ||
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", 0o777, (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, (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(`sh "${found}"`, unlink); | ||
}); | ||
if (typeof compress === "function") { | ||
await new Promise((resolve, reject) => { | ||
this.exec(compress(this.filename, filename), (error, stdout, stderr) => { | ||
if (error) | ||
return reject(error); | ||
this.emit("external", stdout, stderr); | ||
resolve(); | ||
}); | ||
}); | ||
}); | ||
} | ||
else | ||
await this.gzip(filename); | ||
return (0, promises_1.unlink)(this.filename); | ||
} | ||
gzip(filename, callback) { | ||
async gzip(filename) { | ||
const { mode } = this.options; | ||
@@ -432,121 +326,95 @@ const options = mode ? { mode } : {}; | ||
const zip = this.createGzip(); | ||
[inp, out, zip].map(stream => stream.once("error", callback)); | ||
out.once("finish", callback); | ||
inp.pipe(zip).pipe(out); | ||
return new Promise((resolve, reject) => { | ||
[inp, out, zip].map(stream => stream.once("error", reject)); | ||
out.once("finish", resolve); | ||
inp.pipe(zip).pipe(out); | ||
}); | ||
} | ||
rotated(filename, callback) { | ||
async rotated(filename) { | ||
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(); | ||
await this.history(filename); | ||
this.emit("rotated", filename); | ||
return this.reopen(0); | ||
} | ||
history(filename, callback) { | ||
const { history } = this.options; | ||
this.fsReadFile(history, "utf8", (error, data) => { | ||
if (error) { | ||
if (error.code !== "ENOENT") | ||
return callback(error); | ||
return this.historyGather([filename], 0, [], callback); | ||
async history(filename) { | ||
const { history, maxFiles, maxSize } = this.options; | ||
const res = []; | ||
let files = [filename]; | ||
try { | ||
const content = await this.fsReadFile(history, "utf8"); | ||
files = [...content.toString().split("\n"), filename]; | ||
} | ||
catch (e) { | ||
if (e.code !== "ENOENT") | ||
throw e; | ||
} | ||
for (const file of files) { | ||
if (file) { | ||
try { | ||
const stats = await this.fsStat(file); | ||
if (stats.isFile()) { | ||
res.push({ | ||
name: file, | ||
size: stats.size, | ||
time: stats.ctime.getTime() | ||
}); | ||
} | ||
else | ||
this.emit("warning", new Error(`File '${file}' contained in history is not a regular file`)); | ||
} | ||
catch (e) { | ||
if (e.code !== "ENOENT") | ||
throw e; | ||
} | ||
} | ||
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); | ||
} | ||
res.sort((a, b) => a.time - b.time); | ||
if (maxFiles) { | ||
while (res.length > maxFiles) { | ||
const file = res.shift(); | ||
await (0, promises_1.unlink)(file.name); | ||
this.emit("removed", file.name, true); | ||
} | ||
else if (stats.isFile()) { | ||
res.push({ | ||
name: files[index], | ||
size: stats.size, | ||
time: stats.ctime.getTime() | ||
}); | ||
} | ||
if (maxSize) { | ||
while (res.reduce((size, file) => size + file.size, 0) > maxSize) { | ||
const file = res.shift(); | ||
await (0, promises_1.unlink)(file.name); | ||
this.emit("removed", file.name, false); | ||
} | ||
else | ||
this.emit("warning", new Error(`File '${files[index]}' contained in history is not a regular file`)); | ||
this.historyGather(files, index + 1, res, callback); | ||
}); | ||
} | ||
await (0, promises_1.writeFile)(history, res.map(e => e.name).join("\n") + "\n", "utf-8"); | ||
this.emit("history"); | ||
} | ||
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 { | ||
async immutate(first) { | ||
const { size } = this.options; | ||
const now = this.now(); | ||
for (let index = 1; index < 1000; ++index) { | ||
let fileSize = 0; | ||
let stats = undefined; | ||
this.filename = this.options.path + this.generator(now, index); | ||
} | ||
catch (e) { | ||
return callback(e); | ||
} | ||
const open = (size, callback) => { | ||
try { | ||
stats = await this.fsStat(this.filename); | ||
} | ||
catch (e) { | ||
if (e.code !== "ENOENT") | ||
throw e; | ||
} | ||
if (stats) { | ||
fileSize = stats.size; | ||
if (!stats.isFile()) | ||
throw new Error(`Can't write on: '${this.filename}' (it is not a file)`); | ||
if (size && fileSize >= size) | ||
continue; | ||
} | ||
if (first) { | ||
this.last = this.filename; | ||
return this.reopen(false, size, callback); | ||
return this.reopen(fileSize); | ||
} | ||
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); | ||
}); | ||
await this.rotated(this.last); | ||
this.last = this.filename; | ||
return; | ||
} | ||
throw new RotatingFileStreamError(); | ||
} | ||
@@ -583,9 +451,3 @@ } | ||
} | ||
const intervalUnits = { | ||
M: true, | ||
d: true, | ||
h: true, | ||
m: true, | ||
s: true | ||
}; | ||
const intervalUnits = { M: true, d: true, h: true, m: true, s: true }; | ||
function checkIntervalUnit(ret, unit, amount) { | ||
@@ -610,8 +472,3 @@ if (parseInt((amount / ret.num), 10) * ret.num !== amount) | ||
} | ||
const sizeUnits = { | ||
B: true, | ||
G: true, | ||
K: true, | ||
M: true | ||
}; | ||
const sizeUnits = { B: true, G: true, K: true, M: true }; | ||
function checkSize(value) { | ||
@@ -628,2 +485,14 @@ const ret = checkMeasure(value, "size", sizeUnits); | ||
const checks = { | ||
encoding: (type, options, value) => new util_1.TextDecoder(value), | ||
immutable: () => { }, | ||
initialRotation: () => { }, | ||
interval: buildStringCheck("interval", checkInterval), | ||
intervalBoundary: () => { }, | ||
maxFiles: buildNumberCheck("maxFiles"), | ||
maxSize: buildStringCheck("maxSize", checkSize), | ||
mode: () => { }, | ||
omitExtension: () => { }, | ||
rotate: buildNumberCheck("rotate"), | ||
size: buildStringCheck("size", checkSize), | ||
teeToStdout: () => { }, | ||
compress: (type, options, value) => { | ||
@@ -641,3 +510,2 @@ if (!value) | ||
}, | ||
encoding: (type, options, value) => new util_1.TextDecoder(value), | ||
history: (type) => { | ||
@@ -647,9 +515,2 @@ if (type !== "string") | ||
}, | ||
immutable: () => { }, | ||
initialRotation: () => { }, | ||
interval: buildStringCheck("interval", checkInterval), | ||
intervalBoundary: () => { }, | ||
maxFiles: buildNumberCheck("maxFiles"), | ||
maxSize: buildStringCheck("maxSize", checkSize), | ||
mode: () => { }, | ||
path: (type, options, value) => { | ||
@@ -660,6 +521,3 @@ if (type !== "string") | ||
options.path = value + path_1.sep; | ||
}, | ||
rotate: buildNumberCheck("rotate"), | ||
size: buildStringCheck("size", checkSize), | ||
teeToStdout: () => { } | ||
} | ||
}; | ||
@@ -696,6 +554,6 @@ function checkOpts(options) { | ||
} | ||
function createClassical(filename) { | ||
return (index) => (index ? `${filename}.${index}` : filename); | ||
function createClassical(filename, compress, omitExtension) { | ||
return (index) => (index ? `${filename}.${index}${compress && !omitExtension ? ".gz" : ""}` : filename); | ||
} | ||
function createGenerator(filename) { | ||
function createGenerator(filename, compress, omitExtension) { | ||
const pad = (num) => (num > 9 ? "" : "0") + num; | ||
@@ -709,3 +567,3 @@ return (time, index) => { | ||
const minute = pad(time.getMinutes()); | ||
return month + day + "-" + hour + minute + "-" + pad(index) + "-" + filename; | ||
return month + day + "-" + hour + minute + "-" + pad(index) + "-" + filename + (compress && !omitExtension ? ".gz" : ""); | ||
}; | ||
@@ -719,5 +577,6 @@ } | ||
const opts = checkOpts(options); | ||
const { compress, omitExtension } = opts; | ||
let generator; | ||
if (typeof filename === "string") | ||
generator = options.rotate ? createClassical(filename) : createGenerator(filename); | ||
generator = options.rotate ? createClassical(filename, compress !== undefined, omitExtension) : createGenerator(filename, compress !== undefined, omitExtension); | ||
else if (typeof filename === "function") | ||
@@ -724,0 +583,0 @@ generator = filename; |
127
package.json
{ | ||
"name": "rotating-file-stream", | ||
"version": "2.1.6", | ||
"description": "Opens a stream.Writable to a file rotated by interval and/or size. A logrotate alternative.", | ||
"scripts": { | ||
"all": "npm run eslint && npm run coverage", | ||
"clean": "node -r ts-node/register utils.ts clean", | ||
"coverage": "npm run clean && 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", | ||
"test": "npm run clean && mocha -r ts-node/register test/*ts" | ||
}, | ||
"bugs": "https://github.com/iccicci/rotating-file-stream/issues", | ||
"repository": "https://github.com/iccicci/rotating-file-stream", | ||
"keywords": [ | ||
"log", | ||
"rotate", | ||
"logrotate" | ||
], | ||
"engines": { | ||
"node": ">=10.0" | ||
}, | ||
"author": "Daniele Ricci <daniele.icc@gmail.com> (https://github.com/iccicci)", | ||
"contributors": [ | ||
"cicci (https://www.trinityteam.it/DanieleRicci#en)", | ||
"allevo", | ||
"kbirger", | ||
"jvassev", | ||
"wangao", | ||
"rakshith-ravi", | ||
"Jorge Silva <jorgemsrs@gmail.com>", | ||
"Jan Christoph Bernack <jc.bernack@gmail.com>", | ||
"cchare (https://github.com/cchare)" | ||
], | ||
"license": "MIT", | ||
"funding": { | ||
"url": "https://www.blockchain.com/btc/address/12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN" | ||
}, | ||
"readmeFilename": "README.md", | ||
"types": "index.d.ts", | ||
"devDependencies": { | ||
"@types/mocha": "9.0.0", | ||
"@types/node": "16.10.1", | ||
"@typescript-eslint/eslint-plugin": "4.31.2", | ||
"@typescript-eslint/parser": "4.31.2", | ||
"eslint": "7.32.0", | ||
"mocha": "9.1.2", | ||
"nyc": "15.1.0", | ||
"prettier": "2.4.1", | ||
"ts-node": "10.2.1", | ||
"typescript": "4.4.3" | ||
}, | ||
"prettier": { | ||
"arrowParens": "avoid", | ||
"jsxBracketSameLine": true, | ||
"printWidth": 200, | ||
"trailingComma": "none", | ||
"useTabs": true, | ||
"overrides": [ | ||
{ | ||
"files": [ | ||
"*.md" | ||
], | ||
"options": { | ||
"useTabs": false | ||
} | ||
} | ||
] | ||
} | ||
"name": "rotating-file-stream", | ||
"version": "3.0.0", | ||
"description": "Opens a stream.Writable to a file rotated by interval and/or size. A logrotate alternative.", | ||
"scripts": { | ||
"all": "npm run eslint && npm run coverage", | ||
"clean": "node -r ts-node/register utils.ts clean", | ||
"coverage": "npm run clean && 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", | ||
"test": "npm run clean && mocha -r ts-node/register test/*ts" | ||
}, | ||
"bugs": "https://github.com/iccicci/rotating-file-stream/issues", | ||
"repository": "https://github.com/iccicci/rotating-file-stream", | ||
"keywords": [ | ||
"log", | ||
"rotate", | ||
"logrotate" | ||
], | ||
"engines": { | ||
"node": ">=10.0" | ||
}, | ||
"author": "Daniele Ricci <daniele.icc@gmail.com> (https://github.com/iccicci)", | ||
"contributors": [ | ||
"cicci (https://www.trinityteam.it/DanieleRicci#en)", | ||
"allevo", | ||
"kbirger", | ||
"jvassev", | ||
"wangao", | ||
"rakshith-ravi", | ||
"Jorge Silva <jorgemsrs@gmail.com>", | ||
"Jan Christoph Bernack <jc.bernack@gmail.com>", | ||
"cchare (https://github.com/cchare)" | ||
], | ||
"license": "MIT", | ||
"funding": { | ||
"url": "https://www.blockchain.com/btc/address/12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN" | ||
}, | ||
"readmeFilename": "README.md", | ||
"types": "index.d.ts", | ||
"devDependencies": { | ||
"@types/mocha": "9.0.0", | ||
"@types/node": "16.11.6", | ||
"@typescript-eslint/eslint-plugin": "5.3.0", | ||
"@typescript-eslint/parser": "5.3.0", | ||
"eslint": "8.1.0", | ||
"mocha": "9.1.3", | ||
"nyc": "15.1.0", | ||
"prettier": "2.4.1", | ||
"ts-node": "10.4.0", | ||
"typescript": "4.4.4" | ||
}, | ||
"prettier": { | ||
"arrowParens": "avoid", | ||
"jsxBracketSameLine": true, | ||
"printWidth": 200, | ||
"trailingComma": "none" | ||
} | ||
} |
@@ -6,12 +6,10 @@ # rotating-file-stream | ||
[![Test Coverage][cover-badge]][code-url] | ||
[![Donate][donate-badge]][donate-url] | ||
[![NPM version][npm-badge]][npm-url] | ||
[![Types][types-badge]][npm-url] | ||
[![NPM downloads][npm-downloads-badge]][npm-url] | ||
[![Stars][stars-badge]][github-url] | ||
[![Dependencies][dep-badge]][dep-url] | ||
[![Dev Dependencies][dev-dep-badge]][dev-dep-url] | ||
[![Types][types-badge]][npm-url] | ||
[![Dependents][deps-badge]][npm-url] | ||
[![Donate][donate-badge]][donate-url] | ||
@@ -21,7 +19,3 @@ [code-badge]: https://codeclimate.com/github/iccicci/rotating-file-stream/badges/gpa.svg | ||
[cover-badge]: https://codeclimate.com/github/iccicci/rotating-file-stream/badges/coverage.svg | ||
[dep-badge]: https://david-dm.org/iccicci/rotating-file-stream.svg | ||
[dep-url]: https://david-dm.org/iccicci/rotating-file-stream | ||
[deps-badge]: https://badgen.net/npm/dependents/rotating-file-stream?icon=npm | ||
[dev-dep-badge]: https://david-dm.org/iccicci/rotating-file-stream/dev-status.svg | ||
[dev-dep-url]: https://david-dm.org/iccicci/rotating-file-stream?type=dev | ||
[donate-badge]: https://badgen.net/badge/donate/bitcoin?icon=bitcoin | ||
@@ -64,3 +58,4 @@ [donate-url]: https://blockchain.info/address/12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN | ||
- [Upgrading from v1.x.x to v2.x.x](#upgrading-from-v1xx-to-v2xx) | ||
- [Upgrading from v2 to v3](#upgrading-from-v2-to-v3) | ||
- [Upgrading from v1 to v2](#upgrading-from-v1-to-v2) | ||
- [API](#api) | ||
@@ -72,2 +67,3 @@ - [rfs.createStream(filename[, options])](#rfscreatestreamfilename-options) | ||
- [Class: RotatingFileStream](#class-rotatingfilestream) | ||
- [Event: 'external'](#event-external) | ||
- [Event: 'history'](#event-history) | ||
@@ -90,2 +86,3 @@ - [Event: 'open'](#event-open) | ||
- [mode](#mode) | ||
- [omitExtension](#omitextension) | ||
- [path](#path) | ||
@@ -104,4 +101,21 @@ - [rotate](#rotate) | ||
# Upgrading from v1.x.x to v2.x.x | ||
# Upgrading from v2 to v3 | ||
In **v3** the package was completely refactored using **async / await**. | ||
**TypeScript** types for events and the [external](#event-external) event were added. | ||
**Breaking change**: by default the `.gz` extension is added to the rotated compressed files. | ||
**Breaking change**: the way the _external compression command_ is executed was slightly changed; possible bracking | ||
change. | ||
To maintain back compatibility upgrading from **v2** to **v3**, just follow this rules: | ||
- using a _file name generator_ or not using [`options.compress`](#compress): nothing to do | ||
- using a _file name_ and using [`options.rotation`](#rotation): use [`options.omitExtension`](#omitextension) or check | ||
how rotated files are treated. | ||
# Upgrading from v1 to v2 | ||
There are two main changes in package interface. | ||
@@ -122,4 +136,4 @@ | ||
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: | ||
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: | ||
@@ -216,2 +230,12 @@ - using [`options.rotation`](#rotation): nothing to do | ||
### Event: 'external' | ||
- `stdout` [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The | ||
standard output of the external compression command. | ||
- `stderr` [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The | ||
standard error of the external compression command. | ||
The `external` event is emitted once an _external compression command_ completes its execution to give access to the | ||
command output streams. | ||
### Event: 'history' | ||
@@ -292,2 +316,5 @@ | ||
**Default:** `0o666`. | ||
- [`omitExtension`](#omitextension): | ||
[<boolean>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type) | ||
Omits the `.gz` extension from compressed rotated files. **Default:** `null`. | ||
- [`path`](#path): | ||
@@ -421,2 +448,7 @@ [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) | ||
### omitExtension | ||
From **v3** the package adds by default the `.gz` extension to the rotated compressed files. Simultaneously this option | ||
was added: set this option to `true` to not add the extension, i.e. to keep backward compatibility. | ||
### initialRotation | ||
@@ -523,5 +555,5 @@ | ||
Requires **Node.js v10.x**. | ||
Requires **Node.js v12.x**. | ||
The package is tested under [all Node.js versions](https://travis-ci.org/iccicci/rotating-file-stream) | ||
The package is tested under [all Node.js versions](https://app.travis-ci.com/github/iccicci/rotating-file-stream) | ||
currently supported accordingly to [Node.js Release](https://github.com/nodejs/Release#readme). | ||
@@ -537,28 +569,4 @@ | ||
Exported in **TypeScript**. | ||
**TypeScript** types are distibuted with the package itself. | ||
```typescript | ||
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?: BufferEncoding; | ||
history?: string; | ||
immutable?: boolean; | ||
initialRotation?: boolean; | ||
interval?: string; | ||
intervalBoundary?: boolean; | ||
maxFiles?: number; | ||
maxSize?: string; | ||
mode?: number; | ||
path?: string; | ||
rotate?: number; | ||
size?: string; | ||
teeToStdout?: boolean; | ||
} | ||
export declare class RotatingFileStream extends Writable {} | ||
export declare function createStream(filename: string | Generator, options?: Options): RotatingFileStream; | ||
``` | ||
# Licence | ||
@@ -565,0 +573,0 @@ |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
576
58104
693
3