✅ ЧТО СДЕЛАНО: - Поднят новый 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)
299 lines
11 KiB
JavaScript
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);
|
|
});
|
|
|