ufilesync
Version:
574 lines (498 loc) • 15.9 kB
JavaScript
;
/**
* Created by MJ on 02.09.2016.
*
* @description Суть всего чтобы переопределить методы модуля для работы с файловой системой и после отработки слать задачу на синхронизацию данных
*/
const _ = require('lodash');
const fs = require('fs-extra');
const async = require('async');
const path = require('path');
const amqplib = require('amqplib/callback_api');
const config = require('../config');
const exceptions = require('./exceptions');
const tasks = require('./tasks');
const DecoratorFactory = require('./decorator_factory');
class USync {
constructor(_config, logger) {
const me = this;
if (_config) me.config = _.extend(config, _config);
me.logger = logger;
this.processedLetters = '-_abcdefghijklmnopqrstuvwxyz0123456789'.split('');
// Готовим регулярку на обработку нужных директорий
if (!me.config.regExpFindDirs) me.config.regExpFindDirs = new RegExp(`.*(${me.config.watchDirs.join('|')})/`, 'i');
// Флаг показывает, готовы ли мы сейчас отправлять задачи в очередь
me.isReadyPush = false;
if (!me.config.regExpFindPathStorage)
me.config.regExpFindPathStorage = new RegExp(`^((${this.config.watchDirs.join('|')})/\\w\\/\\w\\/\\w\\/)(.*)$`);
me.possibleQueues = {};
me.isConnected = false;
// массивы с коллбэками по действиям
me.onAction = {
ready: [],
connected: [],
error: [],
push: [],
resend: [],
};
// Чтобы всегда под рукой иметь эти объекты, заводим их тут
me.tasks = tasks;
me.exceptions = exceptions;
me.decoratorFactory = new DecoratorFactory(me, me.config, me.logger);
// Расширяем, запрещаем, разрешаем методы модуля fs
me.fs = me.decoratorFactory.wrapFs(fs);
// Если синхронизация остановлена, то ничего не отправляем в очередь
if (me.config.isRunSync === false) {
if (me.config.isRunDebugMode) {
me.logger.debug({
message: 'Synchronization statics not used',
config: me.config,
});
}
me.isReadyPush = false;
me.fireEventReady();
} else {
me.connectToRabbitMq();
}
}
/**
* Запуск коннекта к RabbitMq
*/
connectToRabbitMq() {
const me = this;
amqplib.connect(me.config.rabbitmq.connectionConfig, function(err, connection) {
if (err) {
me.logger.error({
message: 'Error when connecting to RabbitMQ',
connectionConfig: me.config.rabbitmq.connectionConfig,
error: err,
errorMessage: err.message,
});
return;
}
me.rbmqConnection = connection;
me.fireEventConnected();
me.createChannelRabbitMq(() => {
me.prepareQueue(() => {
me.isReadyPush = true;
me.isConnected = true;
me.fireEventReady();
});
});
});
}
/**
* Создать канал RabbitMq
* @param cb
*/
createChannelRabbitMq(cb) {
const me = this;
// me.rbmqConnection.createChannel((err, channel) => {
me.rbmqConnection.createConfirmChannel((err, channel) => {
if (err) {
me.logger.error({
message: 'Error when creating channel',
error: err,
errorMessage: err.message,
});
return;
}
me.channel = channel;
me.channel.on('error', err => {
me.logger.error({
message: 'Channel signals an error',
error: err,
errorMessage: err.message,
});
});
me.channel.on('close', () => {
if (me.config.isRunDebugMode) {
me.logger.debug({ message: "Channel event 'close'" });
}
});
me.channel.on('return', message => {
const uSyncError = new me.exceptions.ErrorRBMQ(`Return message! ${message.toString()}`);
me.logger.error({
message: 'Channel return message',
channelMessage: message.toString(),
error: uSyncError,
errorMessage: uSyncError.message,
});
});
cb();
});
}
/**
* Пересоздать канал RabbitMq
* @param cb
*/
restartChannelRabbitMq(cb) {
const me = this;
// Так как тварь channel может уже отвалиться и объект не будет работать(но будет в памяти), если к нему обратиться, упадет с ошибкой
try {
me.channel.close(err => {
if (err) {
me.logger.error({
message: 'Error when closing channel (for restart)',
error: err,
errorMessage: err.message,
});
}
me.createChannelRabbitMq(cb);
});
} catch (err) {
me.logger.error({
message: 'Error when closing channel (for restart)',
error: err,
errorMessage: err.message,
});
me.createChannelRabbitMq(cb);
}
}
static eachQueues(letters, maxCount, prefix, idx, iterator, cb) {
async.mapSeries(
letters,
(letter, cb) => {
if (idx >= maxCount) {
iterator(prefix + '_' + letter, cb);
} else {
USync.eachQueues(letters, maxCount, prefix + '_' + letter, idx + 1, iterator, cb);
}
},
cb,
);
}
prepareQueue(cb) {
const me = this;
async.each(
me.config.transmitters,
(transmitter, cb) => {
USync.eachQueues(
me.processedLetters,
me.config.levelDeepQueuePostfix,
transmitter.queuePrefix,
1,
(name, cb) => {
// Создаем очередь, чтобы были готовы для отправки в них сообщений
me.channel.assertQueue(name, me.config.rabbitmq.queueConfig);
me.possibleQueues[name] = 1;
cb();
},
cb,
);
},
cb,
);
}
/**
* Обработчик событий
* @param cbName
* @param cb
*/
on(cbName, cb) {
const me = this;
if (me.onAction[cbName]) {
if (cbName === 'ready' && me.isReady) {
cb();
} else if (cbName === 'connected' && me.isConnected) {
cb();
} else {
me.onAction[cbName].push(cb);
}
} else {
const error = new me.exceptions.ErrorUSync(cbName + ' - not exist this action');
me.logger.error({
message: 'Action does not exist',
action: cbName,
error,
errorMessage: error.message,
});
cb(error);
}
}
/**
* Запуск событий, когда все готово для работы модуля
*/
fireEventReady() {
const me = this;
me.isReady = true;
_.map(me.onAction.ready, function(cb) {
cb(me.fs);
});
}
/**
* Запуск событий, когда готов коннект к RabbitMq
*/
fireEventConnected() {
const me = this;
me.isConnected = true;
_.map(me.onAction.connected, function(cb) {
cb();
});
}
/**
* Запуск событий, когда отправляем задачу в RabbitMq
*/
fireEventPush(task) {
const me = this;
_.map(me.onAction.push, cbPush => {
cbPush(null, task);
});
}
fireEventResend(task) {
const me = this;
_.map(me.onAction.resend, cb => {
cb(null, task);
});
}
/**
* Вывод отладочной информации для задачи
*/
debug(message, task, data = {}) {
if (!(task.debug || (this.config.isRunDebugMode && this.config.debugCommands.length === 0))) return;
this.logger.debug({
message,
taskId: task.id,
taskCommand: task.command,
taskTimeElapsed: task.getTimeElapsed(),
...data,
});
}
/**
* Возвращает задачу для отпарвки в очередь
* @param {String} command Метка(Инициатор отпрачки уведовления). В основном для удобного логирования.
* @param {String} path Путь к папке для синхронизации
* @param {String} subject Описание задачи
* @param {Boolean} debug Флаг отладки
* @returns {Object} Task
*/
task(command, path, subject, debug) {
if (!path || command.toString().length === 0) return new this.exceptions.ErrorCreateTask('Param command required');
if (!command || path.toString().length === 0) return new this.exceptions.ErrorCreateTask('Param path required');
if (this.config.isRunDebugMode) {
if (-1 !== this.config.debugCommands.indexOf(command)) {
debug = true;
} else {
debug = false;
}
}
let queueNamePostfix = this.selectQueuePostfix(path.dest || path);
if (queueNamePostfix.length === 0) {
if (debug) {
this.logger.debug({
message: 'No queueName given',
path: path.dest || path,
});
}
return this.taskSkip();
}
try {
if (this.tasks[command]) {
return new this.tasks[command](this.config, path, queueNamePostfix, subject, debug);
} else {
return new this.tasks.Simple(this.config, command, path, queueNamePostfix, subject, null, debug);
}
} catch (err) {
let errUSync = new this.exceptions.ErrorUSync(err.message);
errUSync.stackTasks = err.stackTasks;
this.logger.error({
message: 'Error when creating task with such command',
command,
stackTasks: errUSync.stackTasks,
error: errUSync,
errorMessage: errUSync.message,
});
return errUSync;
}
}
/**
* Возвращает задачу для пропуска синхронизации
* @returns {Skip}
*/
taskSkip() {
return new this.tasks.Skip();
}
/**
* Выбрать очередь, относительно пути
* @param path
* @returns {string}
*/
selectQueuePostfix(path) {
if (this.config.regExpFindDirs.test(path)) {
return path
.replace(this.config.regExpFindDirs, '')
.substr(0, this.config.levelDeepQueuePostfix * 2 - 1)
.replace(/\//gi, '_');
}
return '';
}
/**
* Отправить задачу на синхронизацию
* @param task instanceof Task
*/
push(task, cb) {
const me = this;
// Если не передали колбек, свой лепим, чтобы отловить ошибки
if (typeof cb === 'undefined') {
cb = err => {
if (err) {
this.logger.error({
message: 'Error when pushing task',
taskId: task.id,
taskCommand: task.command,
taskPath: task.path,
error: err,
errorMessage: err.message,
});
}
};
}
// Если синхронизация остановлена, то ничего не отправляем в очередь
if (me.config.isRunSync === false) {
return cb();
}
// Если прилетела ошибка, то возвращаем ее
if (task instanceof me.exceptions.ErrorUSync) {
return cb(task);
}
// Если задача пришла на пропуск синхронизации, то ничего не отправляем в очередь
if (task instanceof me.tasks.Skip) {
return cb();
}
if (!(task instanceof me.tasks.Simple)) {
this.debug('Task is not instance of Simple', task);
return cb(new exceptions.ErrorUSync('Need instanceof Task'));
}
if (!me.channel) return cb(new exceptions.ErrorRBMQ('Not exist channel'));
// Перед отправкой нужно получить всю оставшуюся необходимую информацию
task.fillStatsIfNeed(task.path.dest || task.path.src, err => {
if (err) return cb(err);
setTimeout(() => {
// 1. сохраняем файлы в хранилище(если это требуется)
task.setTimeStartDebug('storage');
async.each(
task.messages,
(message, cb) => {
if (!me.possibleQueues[message.queueName]) {
return cb(new me.exceptions.ErrorTask(task, `Not processed queue: ${message.queueName}`));
}
me.debug('Save in storage begin', task, { queueName: message.queueName });
//Создаем копию файла(чтобы иметь его состояние в момент выполнения операции)
me.saveInStorage(message, cb);
},
err => {
if (err) {
// На случай, если при записи "копии файла" возникла ошибка, возвращать ошибку нельзя!
const uSyncError = new exceptions.ErrorTask(task, err.message);
me.logger.error({
message: 'Error when saving a copy of file',
taskId: task.id,
taskCommand: task.command,
taskPath: task.path,
error: uSyncError,
errorMessage: uSyncError.message,
});
return cb(err);
}
//2. отправляем сообщения в RabbitMq
task.setTimeStartDebug('push');
async.each(
task.messages,
(message, cb) => {
me.debug('Push begin', task, { queueName: message.queueName });
let send = function(cb) {
// Так как тварь channel может уже отвалиться и объект не будет работать(но будет в памяти), если к нему обратиться, упадет с ошибкой
let buffer;
try {
buffer = Buffer.from(JSON.stringify(message));
} catch (err) {
me.logger.error({
message: 'Error when serialising message that sends to queue',
error: err,
errorMessage: err.message,
});
return cb(err);
}
try {
me.channel.sendToQueue(
message.queueName,
buffer,
{
deliveryMode: 2,
mandatory: true,
},
cb,
);
} catch (err) {
me.logger.error({
message: 'Error when sending to channel queue',
queueName: message.queueName,
error: err,
errorMessage: err.message,
});
cb(err);
}
};
// Если не смогли отправить, пробуем пересоздать канал и отправить еще раз(раббит падла еще та)
send(err => {
if (err) {
me.restartChannelRabbitMq(() => {
// Пытаемся отправить сообщение в RabbitMq еще раз
send(err => {
if (err) {
const uSyncError = new exceptions.ErrorTask(task, 'RabbitMq not work');
me.logger.error({
message: 'Error when sending to RabbitMQ',
taskId: task.id,
taskCommand: task.command,
taskPath: task.path,
error: uSyncError,
errorMessage: uSyncError.message,
});
} else {
me.debug('Resend', task, { queueName: message.queueName });
me.fireEventResend(task);
}
cb();
});
});
} else {
cb();
}
});
},
err => {
if (err) {
// На случай, если при записи "копии файла" возникла ошибка, возвращать ошибку нельзя!
const uSyncError = new exceptions.ErrorTask(task, err.message);
me.logger.error({
message: 'Error when saving a copy of file',
taskId: task.id,
taskCommand: task.command,
taskPath: task.path,
error: uSyncError,
errorMessage: uSyncError.message,
});
return cb(err);
}
me.fireEventPush(task);
cb();
},
);
},
);
}, task.timeDelay);
});
}
saveInStorage(message, cb) {
if (-1 != this.config.fileSendMethods.indexOf(message.command)) {
fs.mkdirp(path.dirname(message.path.store), err => {
if (err) return cb(err);
fs.copy(message.path.dest || message.path.src, message.path.store, cb);
});
} else {
cb();
}
}
}
module.exports = USync;