fileSync.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. const debug = require('debug')('log4js:fileSync');
  2. const path = require('path');
  3. const fs = require('fs');
  4. const os = require('os');
  5. const eol = os.EOL;
  6. function touchFile(file, options) {
  7. // attempt to create the directory
  8. const mkdir = (dir) => {
  9. try {
  10. return fs.mkdirSync(dir, { recursive: true });
  11. }
  12. // backward-compatible fs.mkdirSync for nodejs pre-10.12.0 (without recursive option)
  13. catch (e) {
  14. // recursive creation of parent first
  15. if (e.code === 'ENOENT') {
  16. mkdir(path.dirname(dir));
  17. return mkdir(dir);
  18. }
  19. // throw error for all except EEXIST and EROFS (read-only filesystem)
  20. if (e.code !== 'EEXIST' && e.code !== 'EROFS') {
  21. throw e;
  22. }
  23. // EEXIST: throw if file and not directory
  24. // EROFS : throw if directory not found
  25. else {
  26. try {
  27. if (fs.statSync(dir).isDirectory()) {
  28. return dir;
  29. }
  30. throw e;
  31. } catch (err) {
  32. throw e;
  33. }
  34. }
  35. }
  36. };
  37. mkdir(path.dirname(file));
  38. // try to throw EISDIR, EROFS, EACCES
  39. fs.appendFileSync(file, "", { mode: options.mode, flag: options.flags });
  40. }
  41. class RollingFileSync {
  42. constructor(filename, maxLogSize, backups, options) {
  43. debug('In RollingFileStream');
  44. if (maxLogSize < 0) {
  45. throw new Error(`maxLogSize (${maxLogSize}) should be > 0`);
  46. }
  47. this.filename = filename;
  48. this.size = maxLogSize;
  49. this.backups = backups;
  50. this.options = options;
  51. this.currentSize = 0;
  52. function currentFileSize(file) {
  53. let fileSize = 0;
  54. try {
  55. fileSize = fs.statSync(file).size;
  56. } catch (e) {
  57. // file does not exist
  58. touchFile(file, options);
  59. }
  60. return fileSize;
  61. }
  62. this.currentSize = currentFileSize(this.filename);
  63. }
  64. shouldRoll() {
  65. debug('should roll with current size %d, and max size %d', this.currentSize, this.size);
  66. return this.currentSize >= this.size;
  67. }
  68. roll(filename) {
  69. const that = this;
  70. const nameMatcher = new RegExp(`^${path.basename(filename)}`);
  71. function justTheseFiles(item) {
  72. return nameMatcher.test(item);
  73. }
  74. function index(filename_) {
  75. return parseInt(filename_.slice((`${path.basename(filename)}.`).length), 10) || 0;
  76. }
  77. function byIndex(a, b) {
  78. return index(a) - index(b);
  79. }
  80. function increaseFileIndex(fileToRename) {
  81. const idx = index(fileToRename);
  82. debug(`Index of ${fileToRename} is ${idx}`);
  83. if (that.backups === 0) {
  84. fs.truncateSync(filename, 0);
  85. } else if (idx < that.backups) {
  86. // on windows, you can get a EEXIST error if you rename a file to an existing file
  87. // so, we'll try to delete the file we're renaming to first
  88. try {
  89. fs.unlinkSync(`${filename}.${idx + 1}`);
  90. } catch (e) {
  91. // ignore err: if we could not delete, it's most likely that it doesn't exist
  92. }
  93. debug(`Renaming ${fileToRename} -> ${filename}.${idx + 1}`);
  94. fs.renameSync(path.join(path.dirname(filename), fileToRename), `${filename}.${idx + 1}`);
  95. }
  96. }
  97. function renameTheFiles() {
  98. // roll the backups (rename file.n to file.n+1, where n <= numBackups)
  99. debug('Renaming the old files');
  100. const files = fs.readdirSync(path.dirname(filename));
  101. files.filter(justTheseFiles).sort(byIndex).reverse().forEach(increaseFileIndex);
  102. }
  103. debug('Rolling, rolling, rolling');
  104. renameTheFiles();
  105. }
  106. // eslint-disable-next-line no-unused-vars
  107. write(chunk, encoding) {
  108. const that = this;
  109. function writeTheChunk() {
  110. debug('writing the chunk to the file');
  111. that.currentSize += chunk.length;
  112. fs.appendFileSync(that.filename, chunk);
  113. }
  114. debug('in write');
  115. if (this.shouldRoll()) {
  116. this.currentSize = 0;
  117. this.roll(this.filename);
  118. }
  119. writeTheChunk();
  120. }
  121. }
  122. /**
  123. * File Appender writing the logs to a text file. Supports rolling of logs by size.
  124. *
  125. * @param file the file log messages will be written to
  126. * @param layout a function that takes a logevent and returns a string
  127. * (defaults to basicLayout).
  128. * @param logSize - the maximum size (in bytes) for a log file,
  129. * if not provided then logs won't be rotated.
  130. * @param numBackups - the number of log files to keep after logSize
  131. * has been reached (default 5)
  132. * @param options - options to be passed to the underlying stream
  133. * @param timezoneOffset - optional timezone offset in minutes (default system local)
  134. */
  135. function fileAppender(file, layout, logSize, numBackups, options, timezoneOffset) {
  136. if (typeof file !== "string" || file.length === 0) {
  137. throw new Error(`Invalid filename: ${file}`);
  138. } else if (file.endsWith(path.sep)) {
  139. throw new Error(`Filename is a directory: ${file}`);
  140. } else {
  141. // handle ~ expansion: https://github.com/nodejs/node/issues/684
  142. // exclude ~ and ~filename as these can be valid files
  143. file = file.replace(new RegExp(`^~(?=${path.sep}.+)`), os.homedir());
  144. }
  145. file = path.normalize(file);
  146. numBackups = (!numBackups && numBackups !== 0) ? 5 : numBackups;
  147. debug(
  148. 'Creating fileSync appender (',
  149. file, ', ',
  150. logSize, ', ',
  151. numBackups, ', ',
  152. options, ', ',
  153. timezoneOffset, ')'
  154. );
  155. function openTheStream(filePath, fileSize, numFiles) {
  156. let stream;
  157. if (fileSize) {
  158. stream = new RollingFileSync(
  159. filePath,
  160. fileSize,
  161. numFiles,
  162. options
  163. );
  164. } else {
  165. stream = (((f) => {
  166. // touch the file to apply flags (like w to truncate the file)
  167. touchFile(f, options);
  168. return {
  169. write(data) {
  170. fs.appendFileSync(f, data);
  171. }
  172. };
  173. }))(filePath);
  174. }
  175. return stream;
  176. }
  177. const logFile = openTheStream(file, logSize, numBackups);
  178. return (loggingEvent) => {
  179. logFile.write(layout(loggingEvent, timezoneOffset) + eol);
  180. };
  181. }
  182. function configure(config, layouts) {
  183. let layout = layouts.basicLayout;
  184. if (config.layout) {
  185. layout = layouts.layout(config.layout.type, config.layout);
  186. }
  187. const options = {
  188. flags: config.flags || 'a',
  189. encoding: config.encoding || 'utf8',
  190. mode: config.mode || 0o600
  191. };
  192. return fileAppender(
  193. config.filename,
  194. layout,
  195. config.maxLogSize,
  196. config.backups,
  197. options,
  198. config.timezoneOffset
  199. );
  200. }
  201. module.exports.configure = configure;