✅ ЧТО СДЕЛАНО: - Поднят новый 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)
250 lines
8.9 KiB
JavaScript
Executable File
250 lines
8.9 KiB
JavaScript
Executable File
#!/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);
|
||
});
|
||
|