const debug = require('debug')('log4js:fileSync'); const path = require('path'); const fs = require('fs'); const os = require('os'); const eol = os.EOL; function touchFile(file, options) { // attempt to create the directory const mkdir = (dir) => { try { return fs.mkdirSync(dir, { recursive: true }); } catch (e) { // backward-compatible fs.mkdirSync for nodejs pre-10.12.0 (without recursive option) // recursive creation of parent first if (e.code === 'ENOENT') { mkdir(path.dirname(dir)); return mkdir(dir); } // throw error for all except EEXIST and EROFS (read-only filesystem) if (e.code !== 'EEXIST' && e.code !== 'EROFS') { throw e; } // EEXIST: throw if file and not directory // EROFS : throw if directory not found else { try { if (fs.statSync(dir).isDirectory()) { return dir; } throw e; } catch (err) { throw e; } } } }; mkdir(path.dirname(file)); // try to throw EISDIR, EROFS, EACCES fs.appendFileSync(file, '', { mode: options.mode, flag: options.flags }); } class RollingFileSync { constructor(filename, maxLogSize, backups, options) { debug('In RollingFileStream'); if (maxLogSize < 0) { throw new Error(`maxLogSize (${maxLogSize}) should be > 0`); } this.filename = filename; this.size = maxLogSize; this.backups = backups; this.options = options; this.currentSize = 0; function currentFileSize(file) { let fileSize = 0; try { fileSize = fs.statSync(file).size; } catch (e) { // file does not exist touchFile(file, options); } return fileSize; } this.currentSize = currentFileSize(this.filename); } shouldRoll() { debug( 'should roll with current size %d, and max size %d', this.currentSize, this.size ); return this.currentSize >= this.size; } roll(filename) { const that = this; const nameMatcher = new RegExp(`^${path.basename(filename)}`); function justTheseFiles(item) { return nameMatcher.test(item); } function index(filename_) { return ( parseInt(filename_.slice(`${path.basename(filename)}.`.length), 10) || 0 ); } function byIndex(a, b) { return index(a) - index(b); } function increaseFileIndex(fileToRename) { const idx = index(fileToRename); debug(`Index of ${fileToRename} is ${idx}`); if (that.backups === 0) { fs.truncateSync(filename, 0); } else if (idx < that.backups) { // on windows, you can get a EEXIST error if you rename a file to an existing file // so, we'll try to delete the file we're renaming to first try { fs.unlinkSync(`${filename}.${idx + 1}`); } catch (e) { // ignore err: if we could not delete, it's most likely that it doesn't exist } debug(`Renaming ${fileToRename} -> ${filename}.${idx + 1}`); fs.renameSync( path.join(path.dirname(filename), fileToRename), `${filename}.${idx + 1}` ); } } function renameTheFiles() { // roll the backups (rename file.n to file.n+1, where n <= numBackups) debug('Renaming the old files'); const files = fs.readdirSync(path.dirname(filename)); files .filter(justTheseFiles) .sort(byIndex) .reverse() .forEach(increaseFileIndex); } debug('Rolling, rolling, rolling'); renameTheFiles(); } // eslint-disable-next-line no-unused-vars write(chunk, encoding) { const that = this; function writeTheChunk() { debug('writing the chunk to the file'); that.currentSize += chunk.length; fs.appendFileSync(that.filename, chunk); } debug('in write'); if (this.shouldRoll()) { this.currentSize = 0; this.roll(this.filename); } writeTheChunk(); } } /** * File Appender writing the logs to a text file. Supports rolling of logs by size. * * @param file the file log messages will be written to * @param layout a function that takes a logevent and returns a string * (defaults to basicLayout). * @param logSize - the maximum size (in bytes) for a log file, * if not provided then logs won't be rotated. * @param numBackups - the number of log files to keep after logSize * has been reached (default 5) * @param options - options to be passed to the underlying stream * @param timezoneOffset - optional timezone offset in minutes (default system local) */ function fileAppender( file, layout, logSize, numBackups, options, timezoneOffset ) { if (typeof file !== 'string' || file.length === 0) { throw new Error(`Invalid filename: ${file}`); } else if (file.endsWith(path.sep)) { throw new Error(`Filename is a directory: ${file}`); } else if (file.indexOf(`~${path.sep}`) === 0) { // handle ~ expansion: https://github.com/nodejs/node/issues/684 // exclude ~ and ~filename as these can be valid files file = file.replace('~', os.homedir()); } file = path.normalize(file); numBackups = !numBackups && numBackups !== 0 ? 5 : numBackups; debug( 'Creating fileSync appender (', file, ', ', logSize, ', ', numBackups, ', ', options, ', ', timezoneOffset, ')' ); function openTheStream(filePath, fileSize, numFiles) { let stream; if (fileSize) { stream = new RollingFileSync(filePath, fileSize, numFiles, options); } else { stream = ((f) => { // touch the file to apply flags (like w to truncate the file) touchFile(f, options); return { write(data) { fs.appendFileSync(f, data); }, }; })(filePath); } return stream; } const logFile = openTheStream(file, logSize, numBackups); return (loggingEvent) => { logFile.write(layout(loggingEvent, timezoneOffset) + eol); }; } function configure(config, layouts) { let layout = layouts.basicLayout; if (config.layout) { layout = layouts.layout(config.layout.type, config.layout); } const options = { flags: config.flags || 'a', encoding: config.encoding || 'utf8', mode: config.mode || 0o600, }; return fileAppender( config.filename, layout, config.maxLogSize, config.backups, options, config.timezoneOffset ); } module.exports.configure = configure;