diff --git a/package-lock.json b/package-lock.json index fbbbf986..8eea03ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "htmlparser2": "^8.0.1", "jsonwebtoken": "^8.5.1", "libgen": "^2.1.0", - "node-cron": "^3.0.0", "node-ffprobe": "^3.0.0", "node-stream-zip": "^1.15.0", "proper-lockfile": "^4.1.2", @@ -1381,14 +1380,6 @@ "node": ">= 0.6" } }, - "node_modules/node-cron": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.1.tgz", - "integrity": "sha512-RAWZTNn2M5KDIUV/389UX0EXsqvdFAwc9QwHQceh0Ga56dygqSRthqIjwpgZsoDspHGt2rkHdk9Z4RgfPMdALw==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/node-ffprobe": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/node-ffprobe/-/node-ffprobe-3.0.0.tgz", @@ -3022,11 +3013,6 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, - "node-cron": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.1.tgz", - "integrity": "sha512-RAWZTNn2M5KDIUV/389UX0EXsqvdFAwc9QwHQceh0Ga56dygqSRthqIjwpgZsoDspHGt2rkHdk9Z4RgfPMdALw==" - }, "node-ffprobe": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/node-ffprobe/-/node-ffprobe-3.0.0.tgz", diff --git a/package.json b/package.json index f941f7db..2bd2201b 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "htmlparser2": "^8.0.1", "jsonwebtoken": "^8.5.1", "libgen": "^2.1.0", - "node-cron": "^3.0.0", "node-ffprobe": "^3.0.0", "node-stream-zip": "^1.15.0", "proper-lockfile": "^4.1.2", diff --git a/server/libs/nodeCron/background-scheduled-task/daemon.js b/server/libs/nodeCron/background-scheduled-task/daemon.js new file mode 100644 index 00000000..022eb692 --- /dev/null +++ b/server/libs/nodeCron/background-scheduled-task/daemon.js @@ -0,0 +1,19 @@ +const ScheduledTask = require('../scheduled-task'); + +let scheduledTask; + +function register(message){ + const script = require(message.path); + scheduledTask = new ScheduledTask(message.cron, script.task, message.options); + scheduledTask.on('task-done', (result) => { + process.send({ type: 'task-done', result}); + }); + process.send({ type: 'registred' }); +} + +process.on('message', (message) => { + switch(message.type){ + case 'register': + return register(message); + } +}); diff --git a/server/libs/nodeCron/background-scheduled-task/index.js b/server/libs/nodeCron/background-scheduled-task/index.js new file mode 100644 index 00000000..e084f3d4 --- /dev/null +++ b/server/libs/nodeCron/background-scheduled-task/index.js @@ -0,0 +1,67 @@ +const EventEmitter = require('events'); +const path = require('path'); +const { fork } = require('child_process'); +const { getId } = require('../../../utils/index') + +const daemonPath = `${__dirname}/daemon.js`; + +class BackgroundScheduledTask extends EventEmitter { + constructor(cronExpression, taskPath, options) { + super(); + if (!options) { + options = { + scheduled: true, + recoverMissedExecutions: false, + }; + } + this.cronExpression = cronExpression; + this.taskPath = taskPath; + this.options = options; + this.options.name = this.options.name || getId() + + if (options.scheduled) { + this.start(); + } + } + + start() { + this.stop(); + this.forkProcess = fork(daemonPath); + + this.forkProcess.on('message', (message) => { + switch (message.type) { + case 'task-done': + this.emit('task-done', message.result); + break; + } + }); + + let options = this.options; + options.scheduled = true; + + this.forkProcess.send({ + type: 'register', + path: path.resolve(this.taskPath), + cron: this.cronExpression, + options: options + }); + } + + stop() { + if (this.forkProcess) { + this.forkProcess.kill(); + } + } + + pid() { + if (this.forkProcess) { + return this.forkProcess.pid; + } + } + + isRunning() { + return !this.forkProcess.killed; + } +} + +module.exports = BackgroundScheduledTask; \ No newline at end of file diff --git a/server/libs/nodeCron/convert-expression/asterisk-to-range-conversion.js b/server/libs/nodeCron/convert-expression/asterisk-to-range-conversion.js new file mode 100644 index 00000000..327346ee --- /dev/null +++ b/server/libs/nodeCron/convert-expression/asterisk-to-range-conversion.js @@ -0,0 +1,21 @@ +'use strict'; +module.exports = (() => { + function convertAsterisk(expression, replecement){ + if(expression.indexOf('*') !== -1){ + return expression.replace('*', replecement); + } + return expression; + } + + function convertAsterisksToRanges(expressions){ + expressions[0] = convertAsterisk(expressions[0], '0-59'); + expressions[1] = convertAsterisk(expressions[1], '0-59'); + expressions[2] = convertAsterisk(expressions[2], '0-23'); + expressions[3] = convertAsterisk(expressions[3], '1-31'); + expressions[4] = convertAsterisk(expressions[4], '1-12'); + expressions[5] = convertAsterisk(expressions[5], '0-6'); + return expressions; + } + + return convertAsterisksToRanges; +})(); diff --git a/server/libs/nodeCron/convert-expression/index.js b/server/libs/nodeCron/convert-expression/index.js new file mode 100644 index 00000000..52489d97 --- /dev/null +++ b/server/libs/nodeCron/convert-expression/index.js @@ -0,0 +1,69 @@ +'use strict'; + +// SOURCE: https://github.com/node-cron/node-cron +// LICENSE: https://github.com/node-cron/node-cron/blob/master/LICENSE.md + +const monthNamesConversion = require('./month-names-conversion'); +const weekDayNamesConversion = require('./week-day-names-conversion'); +const convertAsterisksToRanges = require('./asterisk-to-range-conversion'); +const convertRanges = require('./range-conversion'); +const convertSteps = require('./step-values-conversion'); + +module.exports = (() => { + + function appendSeccondExpression(expressions) { + if (expressions.length === 5) { + return ['0'].concat(expressions); + } + return expressions; + } + + function removeSpaces(str) { + return str.replace(/\s{2,}/g, ' ').trim(); + } + + // Function that takes care of normalization. + function normalizeIntegers(expressions) { + for (let i = 0; i < expressions.length; i++) { + const numbers = expressions[i].split(','); + for (let j = 0; j < numbers.length; j++) { + numbers[j] = parseInt(numbers[j]); + } + expressions[i] = numbers; + } + return expressions; + } + + /* + * The node-cron core allows only numbers (including multiple numbers e.g 1,2). + * This module is going to translate the month names, week day names and ranges + * to integers relatives. + * + * Month names example: + * - expression 0 1 1 January,Sep * + * - Will be translated to 0 1 1 1,9 * + * + * Week day names example: + * - expression 0 1 1 2 Monday,Sat + * - Will be translated to 0 1 1 1,5 * + * + * Ranges example: + * - expression 1-5 * * * * + * - Will be translated to 1,2,3,4,5 * * * * + */ + function interprete(expression) { + let expressions = removeSpaces(expression).split(' '); + expressions = appendSeccondExpression(expressions); + expressions[4] = monthNamesConversion(expressions[4]); + expressions[5] = weekDayNamesConversion(expressions[5]); + expressions = convertAsterisksToRanges(expressions); + expressions = convertRanges(expressions); + expressions = convertSteps(expressions); + + expressions = normalizeIntegers(expressions); + + return expressions.join(' '); + } + + return interprete; +})(); diff --git a/server/libs/nodeCron/convert-expression/month-names-conversion.js b/server/libs/nodeCron/convert-expression/month-names-conversion.js new file mode 100644 index 00000000..52a7842f --- /dev/null +++ b/server/libs/nodeCron/convert-expression/month-names-conversion.js @@ -0,0 +1,22 @@ +'use strict'; +module.exports = (() => { + const months = ['january','february','march','april','may','june','july', + 'august','september','october','november','december']; + const shortMonths = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', + 'sep', 'oct', 'nov', 'dec']; + + function convertMonthName(expression, items){ + for(let i = 0; i < items.length; i++){ + expression = expression.replace(new RegExp(items[i], 'gi'), parseInt(i, 10) + 1); + } + return expression; + } + + function interprete(monthExpression){ + monthExpression = convertMonthName(monthExpression, months); + monthExpression = convertMonthName(monthExpression, shortMonths); + return monthExpression; + } + + return interprete; +})(); diff --git a/server/libs/nodeCron/convert-expression/range-conversion.js b/server/libs/nodeCron/convert-expression/range-conversion.js new file mode 100644 index 00000000..9b69017d --- /dev/null +++ b/server/libs/nodeCron/convert-expression/range-conversion.js @@ -0,0 +1,39 @@ +'use strict'; +module.exports = ( () => { + function replaceWithRange(expression, text, init, end) { + + const numbers = []; + let last = parseInt(end); + let first = parseInt(init); + + if(first > last){ + last = parseInt(init); + first = parseInt(end); + } + + for(let i = first; i <= last; i++) { + numbers.push(i); + } + + return expression.replace(new RegExp(text, 'i'), numbers.join()); + } + + function convertRange(expression){ + const rangeRegEx = /(\d+)-(\d+)/; + let match = rangeRegEx.exec(expression); + while(match !== null && match.length > 0){ + expression = replaceWithRange(expression, match[0], match[1], match[2]); + match = rangeRegEx.exec(expression); + } + return expression; + } + + function convertAllRanges(expressions){ + for(let i = 0; i < expressions.length; i++){ + expressions[i] = convertRange(expressions[i]); + } + return expressions; + } + + return convertAllRanges; +})(); diff --git a/server/libs/nodeCron/convert-expression/step-values-conversion.js b/server/libs/nodeCron/convert-expression/step-values-conversion.js new file mode 100644 index 00000000..01b13ab3 --- /dev/null +++ b/server/libs/nodeCron/convert-expression/step-values-conversion.js @@ -0,0 +1,30 @@ +'use strict'; + +module.exports = (() => { + function convertSteps(expressions){ + var stepValuePattern = /^(.+)\/(\w+)$/; + for(var i = 0; i < expressions.length; i++){ + var match = stepValuePattern.exec(expressions[i]); + var isStepValue = match !== null && match.length > 0; + if(isStepValue){ + var baseDivider = match[2]; + if(isNaN(baseDivider)){ + throw baseDivider + ' is not a valid step value'; + } + var values = match[1].split(','); + var stepValues = []; + var divider = parseInt(baseDivider, 10); + for(var j = 0; j <= values.length; j++){ + var value = parseInt(values[j], 10); + if(value % divider === 0){ + stepValues.push(value); + } + } + expressions[i] = stepValues.join(','); + } + } + return expressions; + } + + return convertSteps; +})(); diff --git a/server/libs/nodeCron/convert-expression/week-day-names-conversion.js b/server/libs/nodeCron/convert-expression/week-day-names-conversion.js new file mode 100644 index 00000000..03ef8b9a --- /dev/null +++ b/server/libs/nodeCron/convert-expression/week-day-names-conversion.js @@ -0,0 +1,21 @@ +'use strict'; +module.exports = (() => { + const weekDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', + 'friday', 'saturday']; + const shortWeekDays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + + function convertWeekDayName(expression, items){ + for(let i = 0; i < items.length; i++){ + expression = expression.replace(new RegExp(items[i], 'gi'), parseInt(i, 10)); + } + return expression; + } + + function convertWeekDays(expression){ + expression = expression.replace('7', '0'); + expression = convertWeekDayName(expression, weekDays); + return convertWeekDayName(expression, shortWeekDays); + } + + return convertWeekDays; +})(); diff --git a/server/libs/nodeCron/index.js b/server/libs/nodeCron/index.js new file mode 100644 index 00000000..459f161a --- /dev/null +++ b/server/libs/nodeCron/index.js @@ -0,0 +1,64 @@ +'use strict'; + +const ScheduledTask = require('./scheduled-task'); +const BackgroundScheduledTask = require('./background-scheduled-task'); +const validation = require('./pattern-validation'); +const storage = require('./storage'); + +/** + * @typedef {Object} CronScheduleOptions + * @prop {boolean} [scheduled] if a scheduled task is ready and running to be + * performed when the time matches the cron expression. + * @prop {string} [timezone] the timezone to execute the task in. + */ + +/** + * Creates a new task to execute the given function when the cron + * expression ticks. + * + * @param {string} expression The cron expression. + * @param {Function} func The task to be executed. + * @param {CronScheduleOptions} [options] A set of options for the scheduled task. + * @returns {ScheduledTask} The scheduled task. + */ +function schedule(expression, func, options) { + const task = createTask(expression, func, options); + + storage.save(task); + + return task; +} + +function createTask(expression, func, options) { + if (typeof func === 'string') + return new BackgroundScheduledTask(expression, func, options); + + return new ScheduledTask(expression, func, options); +} + +/** + * Check if a cron expression is valid. + * + * @param {string} expression The cron expression. + * @returns {boolean} Whether the expression is valid or not. + */ +function validate(expression) { + try { + validation(expression); + + return true; + } catch (_) { + return false; + } +} + +/** + * Gets the scheduled tasks. + * + * @returns {ScheduledTask[]} The scheduled tasks. + */ +function getTasks() { + return storage.getTasks(); +} + +module.exports = { schedule, validate, getTasks }; diff --git a/server/libs/nodeCron/pattern-validation.js b/server/libs/nodeCron/pattern-validation.js new file mode 100644 index 00000000..5bfca831 --- /dev/null +++ b/server/libs/nodeCron/pattern-validation.js @@ -0,0 +1,124 @@ +'use strict'; + +const convertExpression = require('./convert-expression'); + +const validationRegex = /^(?:\d+|\*|\*\/\d+)$/; + +/** + * @param {string} expression The Cron-Job expression. + * @param {number} min The minimum value. + * @param {number} max The maximum value. + * @returns {boolean} + */ +function isValidExpression(expression, min, max) { + const options = expression.split(','); + + for (const option of options) { + const optionAsInt = parseInt(option, 10); + + if ( + (!Number.isNaN(optionAsInt) && + (optionAsInt < min || optionAsInt > max)) || + !validationRegex.test(option) + ) + return false; + } + + return true; +} + +/** + * @param {string} expression The Cron-Job expression. + * @returns {boolean} + */ +function isInvalidSecond(expression) { + return !isValidExpression(expression, 0, 59); +} + +/** + * @param {string} expression The Cron-Job expression. + * @returns {boolean} + */ +function isInvalidMinute(expression) { + return !isValidExpression(expression, 0, 59); +} + +/** + * @param {string} expression The Cron-Job expression. + * @returns {boolean} + */ +function isInvalidHour(expression) { + return !isValidExpression(expression, 0, 23); +} + +/** + * @param {string} expression The Cron-Job expression. + * @returns {boolean} + */ +function isInvalidDayOfMonth(expression) { + return !isValidExpression(expression, 1, 31); +} + +/** + * @param {string} expression The Cron-Job expression. + * @returns {boolean} + */ +function isInvalidMonth(expression) { + return !isValidExpression(expression, 1, 12); +} + +/** + * @param {string} expression The Cron-Job expression. + * @returns {boolean} + */ +function isInvalidWeekDay(expression) { + return !isValidExpression(expression, 0, 7); +} + +/** + * @param {string[]} patterns The Cron-Job expression patterns. + * @param {string[]} executablePatterns The executable Cron-Job expression + * patterns. + * @returns {void} + */ +function validateFields(patterns, executablePatterns) { + if (isInvalidSecond(executablePatterns[0])) + throw new Error(`${patterns[0]} is a invalid expression for second`); + + if (isInvalidMinute(executablePatterns[1])) + throw new Error(`${patterns[1]} is a invalid expression for minute`); + + if (isInvalidHour(executablePatterns[2])) + throw new Error(`${patterns[2]} is a invalid expression for hour`); + + if (isInvalidDayOfMonth(executablePatterns[3])) + throw new Error( + `${patterns[3]} is a invalid expression for day of month` + ); + + if (isInvalidMonth(executablePatterns[4])) + throw new Error(`${patterns[4]} is a invalid expression for month`); + + if (isInvalidWeekDay(executablePatterns[5])) + throw new Error(`${patterns[5]} is a invalid expression for week day`); +} + +/** + * Validates a Cron-Job expression pattern. + * + * @param {string} pattern The Cron-Job expression pattern. + * @returns {void} + */ +function validate(pattern) { + if (typeof pattern !== 'string') + throw new TypeError('pattern must be a string!'); + + const patterns = pattern.split(' '); + const executablePatterns = convertExpression(pattern).split(' '); + + if (patterns.length === 5) patterns.unshift('0'); + + validateFields(patterns, executablePatterns); +} + +module.exports = validate; diff --git a/server/libs/nodeCron/scheduled-task.js b/server/libs/nodeCron/scheduled-task.js new file mode 100644 index 00000000..d5b4d86c --- /dev/null +++ b/server/libs/nodeCron/scheduled-task.js @@ -0,0 +1,51 @@ +'use strict'; + +const EventEmitter = require('events'); +const Task = require('./task'); +const Scheduler = require('./scheduler'); +const { getId } = require('../../utils/index') + +class ScheduledTask extends EventEmitter { + constructor(cronExpression, func, options) { + super(); + if (!options) { + options = { + scheduled: true, + recoverMissedExecutions: false + }; + } + + this.options = options; + this.options.name = this.options.name || getId() + + this._task = new Task(func); + this._scheduler = new Scheduler(cronExpression, options.timezone, options.recoverMissedExecutions); + + this._scheduler.on('scheduled-time-matched', (now) => { + this.now(now); + }); + + if (options.scheduled !== false) { + this._scheduler.start(); + } + + if (options.runOnInit === true) { + this.now('init'); + } + } + + now(now = 'manual') { + let result = this._task.execute(now); + this.emit('task-done', result); + } + + start() { + this._scheduler.start(); + } + + stop() { + this._scheduler.stop(); + } +} + +module.exports = ScheduledTask; diff --git a/server/libs/nodeCron/scheduler.js b/server/libs/nodeCron/scheduler.js new file mode 100644 index 00000000..72febb4f --- /dev/null +++ b/server/libs/nodeCron/scheduler.js @@ -0,0 +1,49 @@ +'use strict'; + +const EventEmitter = require('events'); +const TimeMatcher = require('./time-matcher'); + +class Scheduler extends EventEmitter{ + constructor(pattern, timezone, autorecover){ + super(); + this.timeMatcher = new TimeMatcher(pattern, timezone); + this.autorecover = autorecover; + } + + start(){ + // clear timeout if exists + this.stop(); + + let lastCheck = process.hrtime(); + let lastExecution = this.timeMatcher.apply(new Date()); + + const matchTime = () => { + const delay = 1000; + const elapsedTime = process.hrtime(lastCheck); + const elapsedMs = (elapsedTime[0] * 1e9 + elapsedTime[1]) / 1e6; + const missedExecutions = Math.floor(elapsedMs / 1000); + + for(let i = missedExecutions; i >= 0; i--){ + const date = new Date(new Date().getTime() - i * 1000); + let date_tmp = this.timeMatcher.apply(date); + if(lastExecution.getTime() < date_tmp.getTime() && (i === 0 || this.autorecover) && this.timeMatcher.match(date)){ + this.emit('scheduled-time-matched', date_tmp); + date_tmp.setMilliseconds(0); + lastExecution = date_tmp; + } + } + lastCheck = process.hrtime(); + this.timeout = setTimeout(matchTime, delay); + }; + matchTime(); + } + + stop(){ + if(this.timeout){ + clearTimeout(this.timeout); + } + this.timeout = null; + } +} + +module.exports = Scheduler; diff --git a/server/libs/nodeCron/storage.js b/server/libs/nodeCron/storage.js new file mode 100644 index 00000000..6c131a4f --- /dev/null +++ b/server/libs/nodeCron/storage.js @@ -0,0 +1,19 @@ +module.exports = (() => { + if(!global.scheduledTasks){ + global.scheduledTasks = new Map(); + } + + return { + save: (task) => { + if(!task.options){ + const uuid = require('uuid'); + task.options = {}; + task.options.name = uuid.v4(); + } + global.scheduledTasks.set(task.options.name, task); + }, + getTasks: () => { + return global.scheduledTasks; + } + }; +})(); \ No newline at end of file diff --git a/server/libs/nodeCron/task.js b/server/libs/nodeCron/task.js new file mode 100644 index 00000000..66f49243 --- /dev/null +++ b/server/libs/nodeCron/task.js @@ -0,0 +1,34 @@ +'use strict'; + +const EventEmitter = require('events'); + +class Task extends EventEmitter{ + constructor(execution){ + super(); + if(typeof execution !== 'function') { + throw 'execution must be a function'; + } + this._execution = execution; + } + + execute(now) { + let exec; + try { + exec = this._execution(now); + } catch (error) { + return this.emit('task-failed', error); + } + + if (exec instanceof Promise) { + return exec + .then(() => this.emit('task-finished')) + .catch((error) => this.emit('task-failed', error)); + } else { + this.emit('task-finished'); + return exec; + } + } +} + +module.exports = Task; + diff --git a/server/libs/nodeCron/time-matcher.js b/server/libs/nodeCron/time-matcher.js new file mode 100644 index 00000000..52a0b8ea --- /dev/null +++ b/server/libs/nodeCron/time-matcher.js @@ -0,0 +1,54 @@ +const validatePattern = require('./pattern-validation'); +const convertExpression = require('./convert-expression'); + +function matchPattern(pattern, value){ + if( pattern.indexOf(',') !== -1 ){ + const patterns = pattern.split(','); + return patterns.indexOf(value.toString()) !== -1; + } + return pattern === value.toString(); +} + +class TimeMatcher{ + constructor(pattern, timezone){ + validatePattern(pattern); + this.pattern = convertExpression(pattern); + this.timezone = timezone; + this.expressions = this.pattern.split(' '); + } + + match(date){ + date = this.apply(date); + + const runOnSecond = matchPattern(this.expressions[0], date.getSeconds()); + const runOnMinute = matchPattern(this.expressions[1], date.getMinutes()); + const runOnHour = matchPattern(this.expressions[2], date.getHours()); + const runOnDay = matchPattern(this.expressions[3], date.getDate()); + const runOnMonth = matchPattern(this.expressions[4], date.getMonth() + 1); + const runOnWeekDay = matchPattern(this.expressions[5], date.getDay()); + + return runOnSecond && runOnMinute && runOnHour && runOnDay && runOnMonth && runOnWeekDay; + } + + apply(date){ + if(this.timezone){ + const dtf = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h23', + fractionalSecondDigits: 3, + timeZone: this.timezone + }); + + return new Date(dtf.format(date)); + } + + return date; + } +} + +module.exports = TimeMatcher; \ No newline at end of file diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js index ec01c794..d9fffc2e 100644 --- a/server/managers/BackupManager.js +++ b/server/managers/BackupManager.js @@ -1,6 +1,6 @@ const Path = require('path') -const cron = require('node-cron') +const cron = require('../libs/nodeCron') const fs = require('fs-extra') const archiver = require('archiver') const StreamZip = require('node-stream-zip') @@ -276,7 +276,7 @@ class BackupManager { reject(err) }) archive.on('progress', ({ fs: fsobj }) => { - const maxBackupSizeInBytes = this.serverSettings.maxBackupSize * 1000 * 1000 * 1000 + const maxBackupSizeInBytes = this.serverSettings.maxBackupSize * 1000 * 1000 * 1000 if (fsobj.processedBytes > maxBackupSizeInBytes) { Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`) archive.abort() diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index 973cb45d..aedda279 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -1,5 +1,5 @@ const fs = require('fs-extra') -const cron = require('node-cron') +const cron = require('../libs/nodeCron') const axios = require('axios') const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')