Files
crm.clientright.ru/crm_extensions/file_storage/nextcloud_activity_monitor.js
Fedor 269c7ea216 feat: OnlyOffice Standalone integration with S3 direct URLs
 ЧТО СДЕЛАНО:
- Поднят новый standalone OnlyOffice Document Server (порт 8083)
- Настроен Nginx для доступа через office.clientright.ru:9443
- Создан open_file_v3_standalone.php для работы с новым OnlyOffice
- Реализована поддержка прямых S3 URL (bucket публичный)
- Добавлен s3_proxy.php с поддержкой Range requests
- Создан onlyoffice_callback.php для сохранения (базовая версия)
- Файлы успешно открываются и загружаются!

⚠️ TODO (на завтра):
- Доработать onlyoffice_callback.php для сохранения обратно в ОРИГИНАЛЬНЫЙ путь в S3
- Добавить Redis маппинг documentKey → S3 path
- Обновить CRM JS для использования open_file_v3_standalone.php
- Протестировать сохранение файлов
- Удалить тестовые файлы

📊 РЕЗУЛЬТАТ:
- OnlyOffice Standalone РАБОТАЕТ! 
- Файлы открываются напрямую из S3 
- Редактор загружается БЫСТРО 
- Автосохранение настроено  (но нужна доработка callback)
2025-11-01 01:02:03 +03:00

250 lines
8.9 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
});