UNPKG

rotating-file-stream

Version:

Opens a stream.Writable to a file rotated by interval and/or size. A logrotate alternative.

193 lines (192 loc) 7.68 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createStream = exports.RotatingFileStream = exports.RotatingFileStreamError = void 0; const node_path_1 = require("node:path"); const node_stream_1 = require("node:stream"); const node_util_1 = require("node:util"); class RotatingFileStreamError extends Error { constructor() { super("Too many destination file attempts"); this.code = "RFS-TOO-MANY"; } } exports.RotatingFileStreamError = RotatingFileStreamError; /* eslint-enable @typescript-eslint/no-unused-vars */ // Dummy implementation: only stores constructor args for now. class RotatingFileStream extends node_stream_1.Writable { constructor(generator, options) { super({ decodeStrings: true, defaultEncoding: options.encoding }); this.generator = generator; this.options = options; } } exports.RotatingFileStream = RotatingFileStream; const sizeUnits = { B: true, G: true, K: true, M: true }; const intervalUnits = { d: true, h: true, m: true, M: true, s: true }; const noopChecker = () => undefined; const assertPositiveInteger = (field, type, value) => { const converted = parseInt(value, 10); if (type !== "number" || converted !== value || converted <= 0) throw new Error(`'${field}' option must be a positive integer number`); }; const parseMeasure = (value, what, units) => { const num = parseInt(value, 10); if (Number.isNaN(num)) throw new Error(`Unknown 'options.${what}' format: ${value}`); if (num <= 0) throw new Error(`A positive integer number is expected for 'options.${what}'`); const unit = value.replace(/^[ 0]*/g, "").slice(String(num).length, String(num).length + 1); if (!unit) throw new Error(`Missing unit for 'options.${what}'`); if (!units[unit]) throw new Error(`Unknown 'options.${what}' unit: ${unit}`); return { num, unit }; }; const parseSize = (value, what) => { const { num, unit } = parseMeasure(value, what, sizeUnits); if (unit === "K") return num * 1024; if (unit === "M") return num * 1048576; if (unit === "G") return num * 1073741824; return num; }; const assertIntervalDivider = (num, unit, amount) => { if (Math.trunc(amount / num) * num !== amount) throw new Error(`An integer divider of ${amount} is expected as ${unit} for 'options.interval'`); }; const parseInterval = (value) => { const ret = parseMeasure(value, "interval", intervalUnits); if (ret.unit === "h") assertIntervalDivider(ret.num, "hours", 24); if (ret.unit === "m") assertIntervalDivider(ret.num, "minutes", 60); if (ret.unit === "s") assertIntervalDivider(ret.num, "seconds", 60); return ret; }; const checkers = { compress: (type, options, value) => { if (value === false) return; if (!value) throw new Error("A value for 'options.compress' must be specified"); if (type === "boolean") { options.compress = (source, dest) => `cat ${source} | gzip -c9 > ${dest}`; return; } if (type === "function") return; if (type !== "string") throw new Error(`Don't know how to handle 'options.compress' type: ${type}`); const method = value; if (method !== "gzip") throw new Error(`Don't know how to handle compression method: ${method}`); }, encoding: (_type, _options, value) => { new node_util_1.TextDecoder(value); }, history: type => { if (type !== "string") throw new Error(`Don't know how to handle 'options.history' type: ${type}`); }, immutable: noopChecker, initialRotation: noopChecker, interval: (type, options, value) => { if (type !== "string") throw new Error(`Don't know how to handle 'options.interval' type: ${type}`); options.interval = parseInterval(value); }, intervalBoundary: noopChecker, intervalUTC: noopChecker, maxFiles: (type, _options, value) => assertPositiveInteger("maxFiles", type, value), maxSize: (type, options, value) => { if (type !== "string") throw new Error(`Don't know how to handle 'options.maxSize' type: ${type}`); options.maxSize = parseSize(value, "maxSize"); }, mode: noopChecker, omitExtension: noopChecker, path: (type, options, value) => { if (type !== "string") throw new Error(`Don't know how to handle 'options.path' type: ${type}`); const path = value; if (!path.endsWith(node_path_1.sep)) options.path = path + node_path_1.sep; }, rotate: (type, _options, value) => assertPositiveInteger("rotate", type, value), size: (type, options, value) => { if (type !== "string") throw new Error(`Don't know how to handle 'options.size' type: ${type}`); options.size = parseSize(value, "size"); }, teeToStdout: noopChecker }; const checkOpts = (options) => { const ret = {}; for (const opt of Object.keys(options)) { const value = options[opt]; const type = typeof value; if (!(opt in checkers)) throw new Error(`Unknown option: ${opt}`); ret[opt] = value; checkers[opt](type, ret, value); } if (!ret.path) ret.path = ""; if (!ret.interval) { delete ret.immutable; delete ret.initialRotation; delete ret.intervalBoundary; delete ret.intervalUTC; } if (ret.rotate) { delete ret.history; delete ret.immutable; delete ret.maxFiles; delete ret.maxSize; delete ret.intervalBoundary; delete ret.intervalUTC; } if (ret.immutable) delete ret.compress; if (!ret.intervalBoundary) delete ret.initialRotation; return ret; }; const pad = (num) => `${num > 9 ? "" : "0"}${num}`; const createClassical = (filename, compress, omitExtension) => (index) => index ? `${filename}.${index}${compress && !omitExtension ? ".gz" : ""}` : filename; const createGenerator = (filename, compress, omitExtension) => (time, index) => { if (!time) return filename; const date = time instanceof Date ? time : new Date(time); const month = `${date.getFullYear()}${pad(date.getMonth() + 1)}`; const day = pad(date.getDate()); const hour = pad(date.getHours()); const minute = pad(date.getMinutes()); return `${month}${day}-${hour}${minute}-${pad(index !== null && index !== void 0 ? index : 0)}-${filename}${compress && !omitExtension ? ".gz" : ""}`; }; const createStream = (filename, options) => { const rawOptions = options; if (rawOptions === undefined) options = {}; else if (typeof rawOptions !== "object" || rawOptions === null) throw new Error(`The "options" argument must be of type object. Received type ${typeof rawOptions}`); else options = rawOptions; const opts = checkOpts(options); const { compress, omitExtension } = opts; let generator; if (typeof filename === "string") { generator = opts.rotate ? createClassical(filename, Boolean(compress), Boolean(omitExtension)) : createGenerator(filename, Boolean(compress), Boolean(omitExtension)); } 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;