fileSync.js 6.4 KB

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