Files
crm.clientright.ru/crm_extensions/file_storage/redis_bridge.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

299 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* Redis Bridge: Nextcloud -> CRM
*
* Слушает события из Nextcloud Redis (notify_storage_update)
* и пересылает их в CRM Redis
*/
const Redis = require('ioredis');
const https = require('https');
const { execSync } = require('child_process');
// Nextcloud API credentials
const NEXTCLOUD_API = {
url: 'https://office.clientright.ru:8443',
username: 'admin',
password: 'tGHKS-3cC9m-7Hggb-65Awk-zxWQE',
auth: Buffer.from('admin:tGHKS-3cC9m-7Hggb-65Awk-zxWQE').toString('base64')
};
const CONFIG = {
nextcloud_redis: {
host: '127.0.0.1',
port: 6380,
password: 'Nextcloud_Redis_Pass_2025!'
},
crm_redis: {
host: '147.45.146.17',
port: 6379,
password: 'CRM_Redis_Pass_2025_Secure!'
},
channels: {
nextcloud: 'notify_storage_update',
crm: 'crm:file:events',
crm_raw: 'crm:file:events:raw'
}
};
// Подключаемся к Nextcloud Redis (для подписки)
const nextcloudRedis = new Redis({
host: CONFIG.nextcloud_redis.host,
port: CONFIG.nextcloud_redis.port,
password: CONFIG.nextcloud_redis.password
});
// Подключаемся к CRM Redis (для публикации)
const crmRedis = new Redis({
host: CONFIG.crm_redis.host,
port: CONFIG.crm_redis.port,
password: CONFIG.crm_redis.password
});
console.log('🚀 Redis Bridge: Nextcloud → CRM');
console.log('================================================================================\n');
// Обработка подключения к Nextcloud Redis
nextcloudRedis.on('connect', () => {
console.log('✅ Подключились к Nextcloud Redis');
console.log(` 📡 ${CONFIG.nextcloud_redis.host}:${CONFIG.nextcloud_redis.port}\n`);
});
// Обработка подключения к CRM Redis
crmRedis.on('connect', () => {
console.log('✅ Подключились к CRM Redis');
console.log(` 📡 ${CONFIG.crm_redis.host}:${CONFIG.crm_redis.port}\n`);
});
// Подписываемся на канал Nextcloud
nextcloudRedis.subscribe(CONFIG.channels.nextcloud, (err, count) => {
if (err) {
console.error('❌ Ошибка подписки на канал Nextcloud:', err.message);
process.exit(1);
}
console.log(`👂 Подписались на канал: ${CONFIG.channels.nextcloud}`);
console.log(` Количество подписок: ${count}\n`);
console.log('🎧 Слушаем события от Nextcloud...\n');
console.log('=' .repeat(80));
});
// Кеш для дедупликации (храним последние file_id на 5 секунд)
const recentEvents = new Map();
const DEDUP_TIMEOUT = 5000; // 5 секунд
// Функция получения полного пути файла через Nextcloud DB
function getFullPath(fileId) {
try {
const query = `SELECT path, name, size, etag, mimepart FROM oc_filecache WHERE fileid = ${fileId}`;
const cmd = `docker exec nextcloud-db mysql -u nextcloud -pnextcloud_password nextcloud -ss -e "${query}" 2>/dev/null`;
const output = execSync(cmd, { encoding: 'utf-8', timeout: 3000 }).trim();
if (output) {
const parts = output.split('\t');
return {
fullPath: (parts[0] + '/' + parts[1]).replace('//', '/'),
name: parts[1],
size: parseInt(parts[2]) || 0,
etag: parts[3] || '',
mimeType: (parts[4] || '') + '/' // частичный mime
};
}
return null;
} catch (err) {
return null;
}
}
// Функция получения деталей файла через WebDAV API
function getFileDetails(fileId, path) {
return new Promise((resolve, reject) => {
try {
// Формируем WebDAV URL (убираем "files/" из пути если есть)
const cleanPath = path.replace(/^files\//, '');
const encodedPath = encodeURI(cleanPath);
const options = {
hostname: 'office.clientright.ru',
port: 8443,
path: `/remote.php/webdav/${encodedPath}`,
method: 'PROPFIND',
headers: {
'Authorization': `Basic ${NEXTCLOUD_API.auth}`,
'Depth': '0'
},
rejectUnauthorized: false
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
// Парсим XML ответ
const size = data.match(/<d:getcontentlength>(\d+)<\/d:getcontentlength>/)?.[1] || 0;
const etag = data.match(/<d:getetag>"?([^<"]+)"?<\/d:getetag>/)?.[1] || '';
const lastModified = data.match(/<d:getlastmodified>([^<]+)<\/d:getlastmodified>/)?.[1] || new Date().toISOString();
const mimeType = data.match(/<d:getcontenttype>([^<]+)<\/d:getcontenttype>/)?.[1] || '';
resolve({
size: parseInt(size),
etag: etag.replace(/"/g, ''),
lastModified: lastModified,
mimeType: mimeType
});
} catch (err) {
resolve({
size: 0,
etag: '',
lastModified: new Date().toISOString(),
mimeType: ''
});
}
});
});
req.on('error', (err) => {
console.error(` ⚠️ HTTP ошибка:`, err.message);
resolve({
size: 0,
etag: '',
lastModified: new Date().toISOString(),
mimeType: ''
});
});
req.setTimeout(5000, () => {
req.destroy();
resolve({
size: 0,
etag: '',
lastModified: new Date().toISOString(),
mimeType: ''
});
});
req.end();
} catch (err) {
resolve({
size: 0,
etag: '',
lastModified: new Date().toISOString(),
mimeType: ''
});
}
});
}
// Обработка сообщений от Nextcloud
nextcloudRedis.on('message', (channel, message) => {
const timestamp = new Date().toISOString();
console.log(`\n${timestamp}`);
console.log(`📨 Получено из Nextcloud [${channel}]:`);
console.log(` ${message}`);
try {
// Парсим сообщение
const event = JSON.parse(message);
// ФИЛЬТР 1: Пропускаем события без пути или file_id
if (!event.path || !event.file_id) {
console.log(` ⏭️ Пропущено: нет path или file_id`);
return;
}
// ФИЛЬТР 2: Пропускаем временные файлы (.part, .lock, cache)
const filename = event.path.split('/').pop();
if (!filename ||
filename.endsWith('.part') ||
filename.endsWith('.lock') ||
filename.startsWith('.') ||
event.path.includes('cache/') ||
event.path.includes('appdata_')) {
console.log(` ⏭️ Пропущено: временный файл (${filename})`);
return;
}
// ФИЛЬТР 3: Дедупликация - если этот file_id уже обрабатывали недавно
const dedupKey = `${event.file_id}_${event.path}`;
if (recentEvents.has(dedupKey)) {
console.log(` ⏭️ Дубликат события для file_id ${event.file_id}`);
return;
}
// Добавляем в кеш дедупликации
recentEvents.set(dedupKey, Date.now());
setTimeout(() => recentEvents.delete(dedupKey), DEDUP_TIMEOUT);
// Публикуем RAW событие в CRM Redis
crmRedis.publish(CONFIG.channels.crm_raw, message)
.catch(err => console.error(' ⚠️ Ошибка публикации RAW:', err.message));
// Простое обогащённое событие (без дополнительных запросов)
const enrichedEvent = {
type: 'file_update',
source: 'nextcloud',
timestamp: timestamp,
storage_id: event.storage,
path: event.path,
file_id: event.file_id,
filename: filename,
action: 'update',
operation: 'update'
};
console.log(` ✅ Обработано:`);
console.log(` 📁 Файл: ${enrichedEvent.filename}`);
console.log(` 🆔 ID: ${enrichedEvent.file_id}`);
console.log(` 📂 Путь: ${enrichedEvent.path}`);
// Публикуем обработанное событие в CRM Redis
crmRedis.publish(CONFIG.channels.crm, JSON.stringify(enrichedEvent))
.then(() => {
console.log(` 📤 Опубликовано в CRM Redis [${CONFIG.channels.crm}]`);
})
.catch(err => {
console.error(` ❌ Ошибка публикации:`, err.message);
});
} catch (err) {
console.error(` ❌ Ошибка парсинга события:`, err.message);
}
console.log('─'.repeat(80));
});
// Обработка ошибок Nextcloud Redis
nextcloudRedis.on('error', (err) => {
console.error('❌ Nextcloud Redis ошибка:', err.message);
});
// Обработка ошибок CRM Redis
crmRedis.on('error', (err) => {
console.error('❌ CRM Redis ошибка:', err.message);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n\n⛔ Остановка Redis Bridge...');
nextcloudRedis.disconnect();
crmRedis.disconnect();
console.log('✅ Отключились от Redis серверов');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n\n⛔ Получен сигнал SIGTERM, останавливаемся...');
nextcloudRedis.disconnect();
crmRedis.disconnect();
process.exit(0);
});