Files

182 lines
6.6 KiB
JavaScript
Raw Permalink 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.
/**
* FarmBox Sync Agent
* Синхронизация локального farm-cmms с облаком
* Работает в фоне, не блокирует локальную работу
*/
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const LOCAL_URL = process.env.LOCAL_CMMS_URL || 'http://localhost:3005';
const CLOUD_URL = process.env.CLOUD_CMMS_URL || 'https://to.zeroday.su';
const INTERVAL_SEC = parseInt(process.env.SYNC_INTERVAL_SEC || '30');
const FARM_ID = process.env.FARM_ID || 'farm_001';
const FARM_NAME = process.env.FARM_NAME || 'Ферма 1';
const SYNC_TOKEN = process.env.SYNC_TOKEN || 'changeme';
const QUEUE_DIR = path.join(__dirname, 'queue');
// Очередь изменений (персистентная — пережидает перезапуск)
if (!fs.existsSync(QUEUE_DIR)) fs.mkdirSync(QUEUE_DIR, { recursive: true });
let isOnline = false;
let lastSyncAt = null;
let syncStats = { success: 0, failed: 0, queued: 0 };
// ─── Проверка интернета ────────────────────────────────
async function checkOnline() {
try {
const res = await fetch(`${CLOUD_URL}/api/health`, { timeout: 5000 });
return res.ok;
} catch {
return false;
}
}
// ─── Получить изменения с локального сервера ──────────
async function getLocalChanges(since) {
try {
const res = await fetch(
`${LOCAL_URL}/api/sync/changes?since=${encodeURIComponent(since)}&farm_id=${FARM_ID}`,
{ headers: { 'X-Sync-Token': SYNC_TOKEN }, timeout: 10000 }
);
if (!res.ok) return [];
return res.json();
} catch (err) {
console.error('[Sync] getLocalChanges:', err.message);
return [];
}
}
// ─── Отправить изменения в облако ─────────────────────
async function pushToCloud(changes) {
const res = await fetch(`${CLOUD_URL}/api/sync/receive`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Sync-Token': SYNC_TOKEN,
'X-Farm-Id': FARM_ID,
'X-Farm-Name': FARM_NAME
},
body: JSON.stringify({ changes, farm_id: FARM_ID, farm_name: FARM_NAME }),
timeout: 15000
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// ─── Получить обновления из облака ────────────────────
async function pullFromCloud() {
try {
const res = await fetch(
`${CLOUD_URL}/api/sync/push?farm_id=${FARM_ID}`,
{ headers: { 'X-Sync-Token': SYNC_TOKEN }, timeout: 10000 }
);
if (!res.ok) return [];
return res.json();
} catch {
return [];
}
}
// ─── Применить обновления из облака локально ──────────
async function applyCloudUpdates(updates) {
if (!updates?.length) return;
try {
await fetch(`${LOCAL_URL}/api/sync/apply`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Sync-Token': SYNC_TOKEN
},
body: JSON.stringify({ updates }),
timeout: 10000
});
} catch (err) {
console.error('[Sync] applyCloudUpdates:', err.message);
}
}
// ─── Сохранить в очередь если нет интернета ───────────
function saveToQueue(changes) {
if (!changes?.length) return;
const filename = path.join(QUEUE_DIR, `${Date.now()}.json`);
fs.writeFileSync(filename, JSON.stringify(changes));
syncStats.queued += changes.length;
console.log(`[Sync] 💾 Сохранено в очередь: ${changes.length} изменений`);
}
// ─── Отправить очередь когда появился интернет ────────
async function flushQueue() {
const files = fs.readdirSync(QUEUE_DIR).filter(f => f.endsWith('.json')).sort();
if (!files.length) return;
console.log(`[Sync] 📤 Отправка очереди: ${files.length} файлов`);
for (const file of files) {
try {
const changes = JSON.parse(fs.readFileSync(path.join(QUEUE_DIR, file)));
await pushToCloud(changes);
fs.unlinkSync(path.join(QUEUE_DIR, file));
syncStats.success += changes.length;
} catch (err) {
console.error(`[Sync] Ошибка отправки ${file}:`, err.message);
syncStats.failed++;
break; // Пробуем в следующем цикле
}
}
}
// ─── Основной цикл синхронизации ─────────────────────
let lastChangeTime = new Date(Date.now() - 60 * 60 * 1000).toISOString(); // -1 час при старте
async function syncCycle() {
const online = await checkOnline();
if (online && !isOnline) {
console.log('[Sync] 🟢 Интернет появился — отправляем очередь');
await flushQueue();
} else if (!online && isOnline) {
console.log('[Sync] 🔴 Интернет пропал — работаем локально');
}
isOnline = online;
try {
// Получаем локальные изменения
const changes = await getLocalChanges(lastChangeTime);
if (changes.length > 0) {
if (isOnline) {
await pushToCloud(changes);
lastSyncAt = new Date().toISOString();
syncStats.success += changes.length;
console.log(`[Sync] ✅ Отправлено в облако: ${changes.length} изменений`);
} else {
saveToQueue(changes);
}
lastChangeTime = new Date().toISOString();
}
// Получаем обновления из облака
if (isOnline) {
const updates = await pullFromCloud();
if (updates?.length > 0) {
await applyCloudUpdates(updates);
console.log(`[Sync] ⬇️ Получено из облака: ${updates.length} обновлений`);
}
}
} catch (err) {
console.error('[Sync] Ошибка цикла:', err.message);
}
}
// ─── Запуск ───────────────────────────────────────────
console.log(`[Sync] 🚀 Агент синхронизации запущен`);
console.log(`[Sync] Ферма: ${FARM_NAME} (${FARM_ID})`);
console.log(`[Sync] Локальный: ${LOCAL_URL}`);
console.log(`[Sync] Облако: ${CLOUD_URL}`);
console.log(`[Sync] Интервал: ${INTERVAL_SEC}с`);
syncCycle();
setInterval(syncCycle, INTERVAL_SEC * 1000);