Files
crm.clientright.ru/crm_extensions/file_storage/nextcloud_activity_monitor.js

250 lines
8.9 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* Nextcloud Activity Monitor
* Мониторит события файлов через Activity API и публикует в Redis
*/
const https = require('https');
const Redis = require('ioredis');
const CONFIG = {
nextcloud: {
host: 'office.clientright.ru',
port: 8443,
username: 'admin',
password: 'tGHKS-3cC9m-7Hggb-65Awk-zxWQE',
auth: Buffer.from('admin:tGHKS-3cC9m-7Hggb-65Awk-zxWQE').toString('base64')
},
redis: {
host: '147.45.146.17',
port: 6379,
password: 'CRM_Redis_Pass_2025_Secure!'
},
pollInterval: 30000, // 30 секунд
stateKey: 'crm:nextcloud:activity:state'
};
const redis = new Redis({
host: CONFIG.redis.host,
port: CONFIG.redis.port,
password: CONFIG.redis.password
});
// Хранилище последнего обработанного activity_id
let lastActivityId = 0;
// Загрузка состояния из Redis
async function loadState() {
try {
const data = await redis.get(CONFIG.stateKey);
if (data) {
const state = JSON.parse(data);
lastActivityId = state.lastActivityId || 0;
console.log(`📥 Последний обработанный activity_id: ${lastActivityId}`);
} else {
console.log('📥 Состояние пустое (первый запуск)');
}
} catch (err) {
console.error('⚠️ Ошибка загрузки состояния:', err.message);
}
}
// Сохранение состояния в Redis
async function saveState() {
try {
await redis.set(CONFIG.stateKey, JSON.stringify({ lastActivityId }));
} catch (err) {
console.error('⚠️ Ошибка сохранения состояния:', err.message);
}
}
// Получение активностей из Nextcloud API
function getActivities(limit = 100) {
return new Promise((resolve, reject) => {
const options = {
hostname: CONFIG.nextcloud.host,
port: CONFIG.nextcloud.port,
path: `/ocs/v2.php/apps/activity/api/v2/activity?format=json&limit=${limit}`,
method: 'GET',
headers: {
'Authorization': `Basic ${CONFIG.nextcloud.auth}`,
'OCS-APIRequest': 'true'
},
rejectUnauthorized: false
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve(json.ocs.data || []);
} catch (err) {
reject(new Error('Ошибка парсинга JSON: ' + err.message));
}
});
});
req.on('error', (err) => {
reject(err);
});
req.setTimeout(10000, () => {
req.destroy();
reject(new Error('Timeout'));
});
req.end();
});
}
// Публикация события в Redis
async function publishEvent(event) {
console.log(`\n 📢 ${event.type.toUpperCase()}: ${event.filename}`);
console.log(` 🆔 file_id: ${event.file_id}`);
console.log(` 👤 user: ${event.user}`);
try {
await redis.publish('crm:file:events', JSON.stringify(event));
console.log(` ✅ Опубликовано в Redis`);
} catch (err) {
console.error(` ❌ Ошибка публикации:`, err.message);
}
}
// Сканирование новых активностей
async function scanActivities() {
try {
console.log(`\n🔍 Проверка новых событий... (${new Date().toISOString()})`);
const activities = await getActivities(100);
const relevantTypes = ['file_created', 'file_changed', 'file_deleted', 'file_restored'];
let newEvents = 0;
let totalFiles = 0;
// Обрабатываем активности (от новых к старым)
for (const activity of activities) {
// Пропускаем уже обработанные
if (activity.activity_id <= lastActivityId) {
continue;
}
// Только файловые события
if (!relevantTypes.includes(activity.type)) {
continue;
}
newEvents++;
// РАЗБИВАЕМ агрегированные события на отдельные файлы!
if (activity.objects && typeof activity.objects === 'object') {
// Множественное событие - разбиваем
const fileIds = Object.keys(activity.objects);
totalFiles += fileIds.length;
console.log(` 📦 Агрегированное событие: ${fileIds.length} файлов`);
for (const fileId of fileIds) {
const filePath = activity.objects[fileId];
const event = {
type: activity.type,
source: 'nextcloud_activity',
timestamp: activity.datetime,
file_id: parseInt(fileId),
path: filePath,
filename: filePath ? filePath.split('/').pop().replace(/^\//, '') : null,
user: activity.user,
activity_id: activity.activity_id,
action: activity.type.replace('file_', '')
};
await publishEvent(event);
}
} else {
// Одиночное событие
totalFiles++;
const event = {
type: activity.type,
source: 'nextcloud_activity',
timestamp: activity.datetime,
file_id: activity.object_id,
path: activity.object_name,
filename: activity.object_name ? activity.object_name.split('/').pop().replace(/^\//, '') : null,
user: activity.user,
activity_id: activity.activity_id,
action: activity.type.replace('file_', '')
};
await publishEvent(event);
}
// Обновляем последний ID
if (activity.activity_id > lastActivityId) {
lastActivityId = activity.activity_id;
}
}
// Сохраняем состояние
await saveState();
console.log(`\n✅ Сканирование завершено:`);
console.log(` 📊 Новых активностей: ${newEvents}`);
console.log(` 📁 Файлов обработано: ${totalFiles}`);
console.log(` 🆔 Последний activity_id: ${lastActivityId}`);
} catch (err) {
console.error('❌ Ошибка сканирования:', err.message);
}
}
// Запуск
async function start() {
console.log('🚀 Nextcloud Activity Monitor');
console.log('════════════════════════════════════════════════════════════════════════════════');
console.log(`📡 Nextcloud: ${CONFIG.nextcloud.host}:${CONFIG.nextcloud.port}`);
console.log(`📡 Redis: ${CONFIG.redis.host}:${CONFIG.redis.port}`);
console.log(`🔄 Интервал: ${CONFIG.pollInterval / 1000}с`);
console.log('════════════════════════════════════════════════════════════════════════════════\n');
await loadState();
console.log('👂 Начинаем мониторинг...\n');
// Первое сканирование
await scanActivities();
// Периодическое сканирование
setInterval(scanActivities, CONFIG.pollInterval);
}
// Запуск при подключении к Redis
redis.on('connect', () => {
console.log('✅ Подключились к Redis\n');
start();
});
redis.on('error', (err) => {
console.error('❌ Redis ошибка:', err.message);
});
process.on('SIGINT', () => {
console.log('\n\n⛔ Остановка мониторинга...');
redis.disconnect();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n\n⛔ Получен сигнал SIGTERM, останавливаемся...');
redis.disconnect();
process.exit(0);
});