FileStreamRotator.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. 'use strict';
  2. /*!
  3. * FileStreamRotator
  4. * Copyright(c) 2012-2017 Holiday Extras.
  5. * Copyright(c) 2017 Roger C.
  6. * MIT Licensed
  7. */
  8. /**
  9. * Module dependencies.
  10. */
  11. var fs = require('fs');
  12. var path = require('path');
  13. var moment = require('moment');
  14. var crypto = require('crypto');
  15. var EventEmitter = require('events');
  16. /**
  17. * FileStreamRotator:
  18. *
  19. * Returns a file stream that auto-rotates based on date.
  20. *
  21. * Options:
  22. *
  23. * - `filename` Filename including full path used by the stream
  24. *
  25. * - `frequency` How often to rotate. Options are 'daily', 'custom' and 'test'. 'test' rotates every minute.
  26. * If frequency is set to none of the above, a YYYYMMDD string will be added to the end of the filename.
  27. *
  28. * - `verbose` If set, it will log to STDOUT when it rotates files and name of log file. Default is TRUE.
  29. *
  30. * - `date_format` Format as used in moment.js http://momentjs.com/docs/#/displaying/format/. The result is used to replace
  31. * the '%DATE%' placeholder in the filename.
  32. * If using 'custom' frequency, it is used to trigger file change when the string representation changes.
  33. *
  34. * - `size` Max size of the file after which it will rotate. It can be combined with frequency or date format.
  35. * The size units are 'k', 'm' and 'g'. Units need to directly follow a number e.g. 1g, 100m, 20k.
  36. *
  37. * - `max_logs` Max number of logs to keep. If not set, it won't remove past logs. It uses its own log audit file
  38. * to keep track of the log files in a json format. It won't delete any file not contained in it.
  39. * It can be a number of files or number of days. If using days, add 'd' as the suffix.
  40. *
  41. * - `audit_file` Location to store the log audit file. If not set, it will be stored in the root of the application.
  42. *
  43. * - `end_stream` End stream (true) instead of the default behaviour of destroy (false). Set value to true if when writing to the
  44. * stream in a loop, if the application terminates or log rotates, data pending to be flushed might be lost.
  45. *
  46. * - `file_options` An object passed to the stream. This can be used to specify flags, encoding, and mode.
  47. * See https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options. Default `{ flags: 'a' }`.
  48. *
  49. * - `utc` Use UTC time for date in filename. Defaults to 'FALSE'
  50. *
  51. * - `extension` File extension to be appended to the filename. This is useful when using size restrictions as the rotation
  52. * adds a count (1,2,3,4,...) at the end of the filename when the required size is met.
  53. *
  54. * - `watch_log` Watch the current file being written to and recreate it in case of accidental deletion. Defaults to 'FALSE'
  55. *
  56. * - `create_symlink` Create a tailable symlink to the current active log file. Defaults to 'FALSE'
  57. *
  58. * - `symlink_name` Name to use when creating the symbolic link. Defaults to 'current.log'
  59. *
  60. * - `audit_hash_type` Use specified hashing algorithm for audit. Defaults to 'md5'. Use 'sha256' for FIPS compliance.
  61. *
  62. * To use with Express / Connect, use as below.
  63. *
  64. * var rotatingLogStream = require('FileStreamRotator').getStream({filename:"/tmp/test.log", frequency:"daily", verbose: false})
  65. * app.use(express.logger({stream: rotatingLogStream, format: "default"}));
  66. *
  67. * @param {Object} options
  68. * @return {Object}
  69. * @api public
  70. */
  71. var FileStreamRotator = {};
  72. module.exports = FileStreamRotator;
  73. var staticFrequency = ['daily', 'test', 'm', 'h', 'custom'];
  74. var DATE_FORMAT = ('YYYYMMDDHHmm');
  75. /**
  76. * Returns frequency metadata for minute/hour rotation
  77. * @param type
  78. * @param num
  79. * @returns {*}
  80. * @private
  81. */
  82. var _checkNumAndType = function (type, num) {
  83. if (typeof num == 'number') {
  84. switch (type) {
  85. case 'm':
  86. if (num < 0 || num > 60) {
  87. return false;
  88. }
  89. break;
  90. case 'h':
  91. if (num < 0 || num > 24) {
  92. return false;
  93. }
  94. break;
  95. }
  96. return {type: type, digit: num};
  97. }
  98. }
  99. /**
  100. * Returns frequency metadata for defined frequency
  101. * @param freqType
  102. * @returns {*}
  103. * @private
  104. */
  105. var _checkDailyAndTest = function (freqType) {
  106. switch (freqType) {
  107. case 'custom':
  108. case 'daily':
  109. return {type: freqType, digit: undefined};
  110. break;
  111. case 'test':
  112. return {type: freqType, digit: 0};
  113. }
  114. return false;
  115. }
  116. /**
  117. * Returns frequency metadata
  118. * @param frequency
  119. * @returns {*}
  120. */
  121. FileStreamRotator.getFrequency = function (frequency) {
  122. var _f = frequency.toLowerCase().match(/^(\d+)([mh])$/)
  123. if(_f){
  124. return _checkNumAndType(_f[2], parseInt(_f[1]));
  125. }
  126. var dailyOrTest = _checkDailyAndTest(frequency);
  127. if (dailyOrTest) {
  128. return dailyOrTest;
  129. }
  130. return false;
  131. }
  132. /**
  133. * Returns a number based on the option string
  134. * @param size
  135. * @returns {Number}
  136. */
  137. FileStreamRotator.parseFileSize = function (size) {
  138. if(size && typeof size == "string"){
  139. var _s = size.toLowerCase().match(/^((?:0\.)?\d+)([kmg])$/);
  140. if(_s){
  141. switch(_s[2]){
  142. case 'k':
  143. return _s[1]*1024
  144. case 'm':
  145. return _s[1]*1024*1024
  146. case 'g':
  147. return _s[1]*1024*1024*1024
  148. }
  149. }
  150. }
  151. return null;
  152. };
  153. /**
  154. * Returns date string for a given format / date_format
  155. * @param format
  156. * @param date_format
  157. * @param {boolean} utc
  158. * @returns {string}
  159. */
  160. FileStreamRotator.getDate = function (format, date_format, utc) {
  161. date_format = date_format || DATE_FORMAT;
  162. let currentMoment = utc ? moment.utc() : moment().local()
  163. if (format && staticFrequency.indexOf(format.type) !== -1) {
  164. switch (format.type) {
  165. case 'm':
  166. var minute = Math.floor(currentMoment.minutes() / format.digit) * format.digit;
  167. return currentMoment.minutes(minute).format(date_format);
  168. break;
  169. case 'h':
  170. var hour = Math.floor(currentMoment.hour() / format.digit) * format.digit;
  171. return currentMoment.hour(hour).format(date_format);
  172. break;
  173. case 'daily':
  174. case 'custom':
  175. case 'test':
  176. return currentMoment.format(date_format);
  177. }
  178. }
  179. return currentMoment.format(date_format);
  180. }
  181. /**
  182. * Read audit json object from disk or return new object or null
  183. * @param max_logs
  184. * @param audit_file
  185. * @param log_file
  186. * @returns {Object} auditLogSettings
  187. * @property {Object} auditLogSettings.keep
  188. * @property {Boolean} auditLogSettings.keep.days
  189. * @property {Number} auditLogSettings.keep.amount
  190. * @property {String} auditLogSettings.auditLog
  191. * @property {Array} auditLogSettings.files
  192. * @property {String} auditLogSettings.hashType
  193. */
  194. FileStreamRotator.setAuditLog = function (max_logs, audit_file, log_file){
  195. var _rtn = null;
  196. if(max_logs){
  197. var use_days = max_logs.toString().substr(-1);
  198. var _num = max_logs.toString().match(/^(\d+)/);
  199. if(Number(_num[1]) > 0) {
  200. var baseLog = path.dirname(log_file.replace(/%DATE%.+/,"_filename"));
  201. try{
  202. if(audit_file){
  203. var full_path = path.resolve(audit_file);
  204. _rtn = JSON.parse(fs.readFileSync(full_path, { encoding: 'utf-8' }));
  205. }else{
  206. var full_path = path.resolve(baseLog + "/" + ".audit.json")
  207. _rtn = JSON.parse(fs.readFileSync(full_path, { encoding: 'utf-8' }));
  208. }
  209. }catch(e){
  210. if(e.code !== "ENOENT"){
  211. return null;
  212. }
  213. _rtn = {
  214. keep: {
  215. days: false,
  216. amount: Number(_num[1])
  217. },
  218. auditLog: audit_file || baseLog + "/" + ".audit.json",
  219. files: []
  220. };
  221. }
  222. _rtn.keep = {
  223. days: use_days === 'd',
  224. amount: Number(_num[1])
  225. };
  226. }
  227. }
  228. return _rtn;
  229. };
  230. /**
  231. * Write audit json object to disk
  232. * @param {Object} audit
  233. * @param {Object} audit.keep
  234. * @param {Boolean} audit.keep.days
  235. * @param {Number} audit.keep.amount
  236. * @param {String} audit.auditLog
  237. * @param {Array} audit.files
  238. * @param {String} audit.hashType
  239. * @param {Boolean} verbose
  240. */
  241. FileStreamRotator.writeAuditLog = function(audit, verbose){
  242. try{
  243. mkDirForFile(audit.auditLog);
  244. fs.writeFileSync(audit.auditLog, JSON.stringify(audit,null,4));
  245. }catch(e){
  246. if (verbose) {
  247. console.error(new Date(),"[FileStreamRotator] Failed to store log audit at:", audit.auditLog,"Error:", e);
  248. }
  249. }
  250. };
  251. /**
  252. * Removes old log file
  253. * @param file
  254. * @param file.hash
  255. * @param file.name
  256. * @param file.date
  257. * @param file.hashType
  258. * @param {Boolean} verbose
  259. */
  260. function removeFile(file, verbose){
  261. if(file.hash === crypto.createHash(file.hashType).update(file.name + "LOG_FILE" + file.date).digest("hex")){
  262. try{
  263. if (fs.existsSync(file.name)) {
  264. fs.unlinkSync(file.name);
  265. }
  266. }catch(e){
  267. if (verbose) {
  268. console.error(new Date(), "[FileStreamRotator] Could not remove old log file: ", file.name);
  269. }
  270. }
  271. }
  272. }
  273. /**
  274. * Create symbolic link to current log file
  275. * @param {String} logfile
  276. * @param {String} name Name to use for symbolic link
  277. * @param {Boolean} verbose
  278. */
  279. function createCurrentSymLink(logfile, name, verbose) {
  280. let symLinkName = name || "current.log"
  281. let logPath = path.dirname(logfile)
  282. let logfileName = path.basename(logfile)
  283. let current = logPath + "/" + symLinkName
  284. try {
  285. let stats = fs.lstatSync(current)
  286. if(stats.isSymbolicLink()){
  287. fs.unlinkSync(current)
  288. fs.symlinkSync(logfileName, current)
  289. }
  290. } catch (err) {
  291. if(err && err.code == "ENOENT") {
  292. try {
  293. fs.symlinkSync(logfileName, current)
  294. } catch (e) {
  295. if (verbose) {
  296. console.error(new Date(), "[FileStreamRotator] Could not create symlink file: ", current, ' -> ', logfileName);
  297. }
  298. }
  299. }
  300. }
  301. }
  302. /**
  303. *
  304. * @param {String} logfile
  305. * @param {Boolean} verbose
  306. * @param {function} cb
  307. */
  308. function createLogWatcher(logfile, verbose, cb){
  309. if(!logfile) return null
  310. // console.log("Creating log watcher")
  311. try {
  312. let stats = fs.lstatSync(logfile)
  313. return fs.watch(logfile, function(event,filename){
  314. // console.log(Date(), event, filename)
  315. if(event == "rename"){
  316. try {
  317. let stats = fs.lstatSync(logfile)
  318. // console.log("STATS:", stats)
  319. }catch(err){
  320. // console.log("ERROR:", err)
  321. cb(err,logfile)
  322. }
  323. }
  324. })
  325. }catch(err){
  326. if(verbose){
  327. console.log(new Date(),"[FileStreamRotator] Could not add watcher for " + logfile);
  328. }
  329. }
  330. }
  331. /**
  332. * Write audit json object to disk
  333. * @param {String} logfile
  334. * @param {Object} audit
  335. * @param {Object} audit.keep
  336. * @param {Boolean} audit.keep.days
  337. * @param {Number} audit.keep.amount
  338. * @param {String} audit.auditLog
  339. * @param {String} audit.hashType
  340. * @param {Array} audit.files
  341. * @param {EventEmitter} stream
  342. * @param {Boolean} verbose
  343. */
  344. FileStreamRotator.addLogToAudit = function(logfile, audit, stream, verbose){
  345. if(audit && audit.files){
  346. // Based on contribution by @nickbug - https://github.com/nickbug
  347. var index = audit.files.findIndex(function(file) {
  348. return (file.name === logfile);
  349. });
  350. if (index !== -1) {
  351. // nothing to do as entry already exists.
  352. return audit;
  353. }
  354. var time = Date.now();
  355. audit.files.push({
  356. date: time,
  357. name: logfile,
  358. hash: crypto.createHash(audit.hashType).update(logfile + "LOG_FILE" + time).digest("hex")
  359. });
  360. if(audit.keep.days){
  361. var oldestDate = moment().subtract(audit.keep.amount,"days").valueOf();
  362. var recentFiles = audit.files.filter(function(file){
  363. if(file.date > oldestDate){
  364. return true;
  365. }
  366. file.hashType = audit.hashType
  367. removeFile(file, verbose);
  368. stream.emit("logRemoved", file)
  369. return false;
  370. });
  371. audit.files = recentFiles;
  372. }else{
  373. var filesToKeep = audit.files.splice(-audit.keep.amount);
  374. if(audit.files.length > 0){
  375. audit.files.filter(function(file){
  376. file.hashType = audit.hashType
  377. removeFile(file, verbose);
  378. stream.emit("logRemoved", file)
  379. return false;
  380. })
  381. }
  382. audit.files = filesToKeep;
  383. }
  384. FileStreamRotator.writeAuditLog(audit, verbose);
  385. }
  386. return audit;
  387. }
  388. /**
  389. *
  390. * @param options
  391. * @param options.filename
  392. * @param options.frequency
  393. * @param options.verbose
  394. * @param options.date_format
  395. * @param options.size
  396. * @param options.max_logs
  397. * @param options.audit_file
  398. * @param options.file_options
  399. * @param options.utc
  400. * @param options.extension File extension to be added at the end of the filename
  401. * @param options.watch_log
  402. * @param options.create_symlink
  403. * @param options.symlink_name
  404. * @param options.audit_hash_type Hash to be used to add to the audit log (md5, sha256)
  405. * @returns {Object} stream
  406. */
  407. FileStreamRotator.getStream = function (options) {
  408. var frequencyMetaData = null;
  409. var curDate = null;
  410. var self = this;
  411. if (!options.filename) {
  412. console.error(new Date(),"[FileStreamRotator] No filename supplied. Defaulting to STDOUT");
  413. return process.stdout;
  414. }
  415. if (options.frequency) {
  416. frequencyMetaData = self.getFrequency(options.frequency);
  417. }
  418. let auditLog = self.setAuditLog(options.max_logs, options.audit_file, options.filename);
  419. // Thanks to Means88 for PR.
  420. if (auditLog != null) {
  421. auditLog.hashType = (options.audit_hash_type !== undefined ? options.audit_hash_type : 'md5');
  422. }
  423. self.verbose = (options.verbose !== undefined ? options.verbose : true);
  424. var fileSize = null;
  425. var fileCount = 0;
  426. var curSize = 0;
  427. if(options.size){
  428. fileSize = FileStreamRotator.parseFileSize(options.size);
  429. }
  430. var dateFormat = (options.date_format || DATE_FORMAT);
  431. if(frequencyMetaData && frequencyMetaData.type == "daily"){
  432. if(!options.date_format){
  433. dateFormat = "YYYY-MM-DD";
  434. }
  435. if(moment().format(dateFormat) != moment().endOf("day").format(dateFormat) || moment().format(dateFormat) == moment().add(1,"day").format(dateFormat)){
  436. if(self.verbose){
  437. console.log(new Date(),"[FileStreamRotator] Changing type to custom as date format changes more often than once a day or not every day");
  438. }
  439. frequencyMetaData.type = "custom";
  440. }
  441. }
  442. if (frequencyMetaData) {
  443. curDate = (options.frequency ? self.getDate(frequencyMetaData,dateFormat, options.utc) : "");
  444. }
  445. options.create_symlink = options.create_symlink || false;
  446. options.extension = options.extension || ""
  447. var filename = options.filename;
  448. var oldFile = null;
  449. var logfile = filename + (curDate ? "." + curDate : "");
  450. if(filename.match(/%DATE%/)){
  451. logfile = filename.replace(/%DATE%/g,(curDate?curDate:self.getDate(null,dateFormat, options.utc)));
  452. }
  453. if(fileSize){
  454. var lastLogFile = null;
  455. var t_log = logfile;
  456. var f = null;
  457. if(auditLog && auditLog.files && auditLog.files instanceof Array && auditLog.files.length > 0){
  458. var lastEntry = auditLog.files[auditLog.files.length - 1].name;
  459. if(lastEntry.match(t_log)){
  460. var lastCount = lastEntry.match(t_log + "\\.(\\d+)");
  461. // Thanks for the PR contribution from @andrefarzat - https://github.com/andrefarzat
  462. if(lastCount){
  463. t_log = lastEntry;
  464. fileCount = lastCount[1];
  465. }
  466. }
  467. }
  468. if (fileCount == 0 && t_log == logfile) {
  469. t_log += options.extension
  470. }
  471. while(f = fs.existsSync(t_log)){
  472. lastLogFile = t_log;
  473. fileCount++;
  474. t_log = logfile + "." + fileCount + options.extension;
  475. }
  476. if(lastLogFile){
  477. var lastLogFileStats = fs.statSync(lastLogFile);
  478. if(lastLogFileStats.size < fileSize){
  479. t_log = lastLogFile;
  480. fileCount--;
  481. curSize = lastLogFileStats.size;
  482. }
  483. }
  484. logfile = t_log;
  485. } else {
  486. logfile += options.extension
  487. }
  488. if (self.verbose) {
  489. console.log(new Date(),"[FileStreamRotator] Logging to: ", logfile);
  490. }
  491. mkDirForFile(logfile);
  492. var file_options = options.file_options || {flags: 'a'};
  493. var rotateStream = fs.createWriteStream(logfile, file_options);
  494. if ((curDate && frequencyMetaData && (staticFrequency.indexOf(frequencyMetaData.type) > -1)) || fileSize > 0) {
  495. if (self.verbose) {
  496. console.log(new Date(),"[FileStreamRotator] Rotating file: ", frequencyMetaData?frequencyMetaData.type:"", fileSize?"size: " + fileSize:"");
  497. }
  498. var stream = new EventEmitter();
  499. stream.auditLog = auditLog;
  500. stream.end = function(){
  501. rotateStream.end.apply(rotateStream,arguments);
  502. };
  503. BubbleEvents(rotateStream,stream);
  504. stream.on('close', function(){
  505. if (logWatcher) {
  506. logWatcher.close()
  507. }
  508. })
  509. stream.on("new",function(newLog){
  510. // console.log("new log", newLog)
  511. stream.auditLog = self.addLogToAudit(newLog,stream.auditLog, stream, self.verbose)
  512. if(options.create_symlink){
  513. createCurrentSymLink(newLog, options.symlink_name, self.verbose)
  514. }
  515. if(options.watch_log){
  516. stream.emit("addWatcher", newLog)
  517. }
  518. });
  519. var logWatcher;
  520. stream.on("addWatcher", function(newLog){
  521. if (logWatcher) {
  522. logWatcher.close()
  523. }
  524. if(!options.watch_log){
  525. return
  526. }
  527. // console.log("ADDING WATCHER", newLog)
  528. logWatcher = createLogWatcher(newLog, self.verbose, function(err,newLog){
  529. stream.emit('createLog', newLog)
  530. })
  531. })
  532. stream.on("createLog",function(file){
  533. try {
  534. let stats = fs.lstatSync(file)
  535. }catch(err){
  536. if(rotateStream && rotateStream.end == "function"){
  537. rotateStream.end();
  538. }
  539. rotateStream = fs.createWriteStream(file, file_options);
  540. stream.emit('new',file);
  541. BubbleEvents(rotateStream,stream);
  542. }
  543. });
  544. stream.write = (function (str, encoding) {
  545. var newDate = frequencyMetaData ? this.getDate(frequencyMetaData, dateFormat, options.utc) : curDate;
  546. if (newDate != curDate || (fileSize && curSize > fileSize)) {
  547. var newLogfile = filename + (curDate && frequencyMetaData ? "." + newDate : "");
  548. if(filename.match(/%DATE%/) && curDate){
  549. newLogfile = filename.replace(/%DATE%/g,newDate);
  550. }
  551. if(fileSize && curSize > fileSize){
  552. fileCount++;
  553. newLogfile += "." + fileCount + options.extension;
  554. }else{
  555. // reset file count
  556. fileCount = 0;
  557. newLogfile += options.extension
  558. }
  559. curSize = 0;
  560. if (self.verbose) {
  561. console.log(new Date(),require('util').format("[FileStreamRotator] Changing logs from %s to %s", logfile, newLogfile));
  562. }
  563. curDate = newDate;
  564. oldFile = logfile;
  565. logfile = newLogfile;
  566. // Thanks to @mattberther https://github.com/mattberther for raising it again.
  567. if(options.end_stream === true){
  568. rotateStream.end();
  569. }else{
  570. rotateStream.destroy();
  571. }
  572. mkDirForFile(logfile);
  573. rotateStream = fs.createWriteStream(newLogfile, file_options);
  574. stream.emit('new',newLogfile);
  575. stream.emit('rotate',oldFile, newLogfile);
  576. BubbleEvents(rotateStream,stream);
  577. }
  578. rotateStream.write(str, encoding);
  579. // Handle length of double-byte characters
  580. curSize += Buffer.byteLength(str, encoding);
  581. }).bind(this);
  582. process.nextTick(function(){
  583. stream.emit('new',logfile);
  584. })
  585. stream.emit('new',logfile)
  586. return stream;
  587. } else {
  588. if (self.verbose) {
  589. console.log(new Date(),"[FileStreamRotator] File won't be rotated: ", options.frequency, options.size);
  590. }
  591. process.nextTick(function(){
  592. rotateStream.emit('new',logfile);
  593. })
  594. return rotateStream;
  595. }
  596. }
  597. /**
  598. * Check and make parent directory
  599. * @param pathWithFile
  600. */
  601. var mkDirForFile = function(pathWithFile){
  602. var _path = path.dirname(pathWithFile);
  603. _path.split(path.sep).reduce(
  604. function(fullPath, folder) {
  605. fullPath += folder + path.sep;
  606. // Option to replace existsSync as deprecated. Maybe in a future release.
  607. // try{
  608. // var stats = fs.statSync(fullPath);
  609. // console.log('STATS',fullPath, stats);
  610. // }catch(e){
  611. // fs.mkdirSync(fullPath);
  612. // console.log("STATS ERROR",e)
  613. // }
  614. if (!fs.existsSync(fullPath)) {
  615. try{
  616. fs.mkdirSync(fullPath);
  617. }catch(e){
  618. if(e.code !== 'EEXIST'){
  619. throw e;
  620. }
  621. }
  622. }
  623. return fullPath;
  624. },
  625. ''
  626. );
  627. };
  628. /**
  629. * Bubbles events to the proxy
  630. * @param emitter
  631. * @param proxy
  632. * @constructor
  633. */
  634. var BubbleEvents = function BubbleEvents(emitter,proxy){
  635. emitter.on('close',function(){
  636. proxy.emit('close');
  637. })
  638. emitter.on('finish',function(){
  639. proxy.emit('finish');
  640. })
  641. emitter.on('error',function(err){
  642. proxy.emit('error',err);
  643. })
  644. emitter.on('open',function(fd){
  645. proxy.emit('open',fd);
  646. })
  647. }