UNPKG

cloudloyaldb

Version:

Centralized NoSQL Database - MongoDB benzeri API ile cloud server'a veri saklama, real-time sync

1,089 lines (908 loc) 35.2 kB
/** * CloudLoyalDB Server * Centralized database server with flydb.json storage + 24h backup + 2-day cleanup */ const express = require('express'); const WebSocket = require('ws'); const fs = require('fs'); const path = require('path'); const cors = require('cors'); class CloudLoyalDBServer { constructor(options = {}) { this.app = express(); this.wss = null; this.clients = new Map(); this.config = { port: options.port || 3000, wsPort: options.wsPort || 3001, dataDir: path.join(process.cwd(), 'data'), backupDir: path.join(process.cwd(), 'flydb-backups'), corsOrigin: options.corsOrigin || '*' }; // Data storage - Multi-database cache this.dataCache = new Map(); // Ensure data directory exists if (!fs.existsSync(this.config.dataDir)) { fs.mkdirSync(this.config.dataDir, { recursive: true }); console.log('CloudLoyalDB: data/ klasörü oluşturuldu'); } // Backup interval (24 hours = 86400000ms) this.backupInterval = null; this._setupMiddleware(); this._setupRoutes(); this._setupWebSocket(); this._setupAutoBackup(); } /** * Database dosya yolunu al */ getDataFile(database = 'main') { return path.join(this.config.dataDir, `${database}.json`); } /** * Belirtilen database dosyasından verileri yükle */ _loadData(database = 'main') { try { const dataFile = this.getDataFile(database); if (fs.existsSync(dataFile)) { const rawData = fs.readFileSync(dataFile, 'utf8'); console.log(`CloudLoyalDB: ${database}.json yüklendi`); return JSON.parse(rawData || '{}'); } console.log(`CloudLoyalDB: Yeni ${database}.json oluşturuluyor`); return {}; } catch (error) { console.error(`CloudLoyalDB: ${database}.json yükleme hatası:`, error.message); return {}; } } /** * Verileri belirtilen database dosyasına kaydet */ _saveData(database = 'main', data) { try { const dataFile = this.getDataFile(database); const jsonData = JSON.stringify(data, null, 2); fs.writeFileSync(dataFile, jsonData, 'utf8'); console.log(`CloudLoyalDB: ${database}.json kaydedildi (${Object.keys(data).length} koleksiyon)`); } catch (error) { console.error(`CloudLoyalDB: ${database}.json kaydetme hatası:`, error.message); throw error; } } /** * Database cache'den veri al veya dosyadan yükle */ _getDatabase(database = 'main') { if (!this.dataCache.has(database)) { const data = this._loadData(database); this.dataCache.set(database, data); } return this.dataCache.get(database); } /** * Database'i cache'e kaydet ve dosyaya yaz + Direkt Backup */ _setDatabase(database = 'main', data) { this.dataCache.set(database, data); this._saveData(database, data); // Direkt backup oluştur this._createDirectBackup(database, data); } /** * Her veri değişikliğinde direkt backup oluştur */ _createDirectBackup(database, data) { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupFileName = `${database}-data-${timestamp}.json`; const backupPath = path.join(this.config.backupDir, backupFileName); const backupData = { metadata: { type: 'auto-save', database, createdAt: new Date().toISOString(), originalFile: this.getDataFile(database), totalCollections: Object.keys(data || {}).length, serverVersion: '1.0.0' }, data: data || {} }; fs.writeFileSync(backupPath, JSON.stringify(backupData, null, 2), 'utf8'); console.log(`CloudLoyalDB: ${database} direkt backup: ${backupFileName}`); // WebSocket ile bildir this._broadcast({ type: 'data_backup_created', database, backupId: backupFileName, timestamp: Date.now(), backupType: 'auto-save' }); } catch (error) { console.error('Direkt backup hatası:', error.message); } } /** * Middleware kurulumu */ _setupMiddleware() { this.app.use(cors({ origin: this.config.corsOrigin })); this.app.use(express.json({ limit: '10mb' })); this.app.use(express.urlencoded({ extended: true })); // Request logging this.app.use((req, res, next) => { console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); next(); }); } /** * API Routes kurulumu */ _setupRoutes() { // Health check this.app.get('/api/health', (req, res) => { // Calculate total collections across all databases let totalCollections = 0; const databases = []; if (fs.existsSync(this.config.dataDir)) { const dataFiles = fs.readdirSync(this.config.dataDir) .filter(file => file.endsWith('.json')); for (const file of dataFiles) { const dbName = file.replace('.json', ''); const data = this._getDatabase(dbName); const collections = Object.keys(data || {}).length; totalCollections += collections; databases.push({ database: dbName, collections }); } } res.json({ status: 'ok', timestamp: new Date().toISOString(), version: '1.0.0', totalCollections, databases, dataDir: this.config.dataDir }); }); // Data operations this.app.post('/api/data/set', this._handleSet.bind(this)); this.app.get('/api/data/get', this._handleGet.bind(this)); this.app.get('/api/data/all', this._handleGetAll.bind(this)); this.app.post('/api/data/push', this._handlePush.bind(this)); this.app.post('/api/data/unpush', this._handleUnpush.bind(this)); this.app.post('/api/data/delByPriority', this._handleDelByPriority.bind(this)); this.app.post('/api/data/setByPriority', this._handleSetByPriority.bind(this)); this.app.delete('/api/data/delete', this._handleDelete.bind(this)); this.app.delete('/api/data/deleteAll', this._handleDeleteAll.bind(this)); this.app.get('/api/data/has', this._handleHas.bind(this)); // Backup operations this.app.post('/api/backup/manual', this._handleManualBackup.bind(this)); this.app.get('/api/backup/list', this._handleListBackups.bind(this)); this.app.post('/api/backup/restore', this._handleRestoreBackup.bind(this)); // Stats this.app.get('/api/stats', this._handleStats.bind(this)); // Error handler this.app.use((err, req, res, next) => { console.error('Server Error:', err); res.status(500).json({ error: 'Internal server error' }); }); } /** * WebSocket kurulumu */ _setupWebSocket() { this.wss = new WebSocket.Server({ port: this.config.wsPort }); this.wss.on('connection', (ws, req) => { const clientId = Date.now() + Math.random(); this.clients.set(ws, { id: clientId, connectedAt: new Date() }); console.log(`WebSocket Client bağlandı: ${clientId}`); ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); this._handleWebSocketMessage(ws, message); } catch (error) { console.error('WebSocket mesaj hatası:', error.message); } }); ws.on('close', () => { this.clients.delete(ws); console.log(`WebSocket Client ayrıldı: ${clientId}`); }); ws.on('error', (error) => { console.error('WebSocket hatası:', error.message); this.clients.delete(ws); }); // Bağlantı onayı ws.send(JSON.stringify({ type: 'connected', clientId, timestamp: Date.now() })); }); console.log(`CloudLoyalDB: WebSocket Server başlatıldı: ws://localhost:${this.config.wsPort}`); } /** * WebSocket mesaj işleyici */ _handleWebSocketMessage(ws, message) { const { type } = message; switch (type) { case 'ping': ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); break; case 'change_database': // Database değiştirme bildirimi console.log('Client database changed:', message.database); break; } } /** * Tüm client'lara broadcast */ _broadcast(message, excludeWs = null) { this.clients.forEach((clientInfo, ws) => { if (ws !== excludeWs && ws.readyState === WebSocket.OPEN) { try { ws.send(JSON.stringify(message)); } catch (error) { console.error('Broadcast hatası:', error.message); this.clients.delete(ws); } } }); } // ========== API HANDLERS ========== /** * SET - Veri ekleme/güncelleme */ _handleSet(req, res) { try { const { collection, path, value, database = 'main' } = req.body; if (!collection) { return res.status(400).json({ error: 'Collection gereklidir' }); } if (!path) { return res.status(400).json({ error: 'Path gereklidir' }); } // Database verilerini al const data = this._getDatabase(database); // Collection yoksa oluştur if (!data[collection]) { data[collection] = {}; } // Nested path'e veri set et this._setNestedValue(data[collection], path, value); // Database'e kaydet this._setDatabase(database, data); // Real-time broadcast this._broadcast({ type: 'data_changed', database, collection, path, value, operation: 'set', timestamp: Date.now() }); res.json({ success: true, database, path, value }); } catch (error) { console.error('Set Error:', error); res.status(500).json({ error: error.message }); } } /** * GET - Veri alma */ _handleGet(req, res) { try { const { collection, path, database = 'main' } = req.query; if (!collection) { return res.status(400).json({ error: 'Collection gereklidir' }); } // Database verilerini al const data = this._getDatabase(database); if (!data[collection]) { return res.status(404).json({ error: 'Collection bulunamadı' }); } const value = path ? this._getNestedValue(data[collection], path) : data[collection]; res.json({ value }); } catch (error) { console.error('Get Error:', error); res.status(500).json({ error: error.message }); } } /** * ALL - Tüm koleksiyon verisi */ _handleGetAll(req, res) { try { const { collection, database = 'main' } = req.query; if (!collection) { return res.status(400).json({ error: 'Collection gereklidir' }); } const data = this._getDatabase(database); const collectionData = data[collection] || {}; res.json({ data: collectionData }); } catch (error) { console.error('GetAll Error:', error); res.status(500).json({ error: error.message }); } } /** * PUSH - Array'e eleman ekleme */ _handlePush(req, res) { try { const { collection, path, value, database = 'main' } = req.body; if (!collection || !path) { return res.status(400).json({ error: 'Collection ve path gereklidir' }); } // Database verilerini al const data = this._getDatabase(database); // Collection yoksa oluştur if (!data[collection]) { data[collection] = {}; } // Mevcut değeri al let arr = this._getNestedValue(data[collection], path); // Array değilse oluştur if (!Array.isArray(arr)) { arr = []; } // Değeri ekle arr.push(value); // Geri kaydet this._setNestedValue(data[collection], path, arr); this._setDatabase(database, data); // Broadcast this._broadcast({ type: 'data_changed', collection, path, value, operation: 'push', timestamp: Date.now() }); res.json({ success: true, path, newLength: arr.length }); } catch (error) { console.error('Push Error:', error); res.status(500).json({ error: error.message }); } } /** * Stats endpoint */ _handleStats(req, res) { try { let totalCollections = 0; const databaseStats = {}; if (fs.existsSync(this.config.dataDir)) { const dataFiles = fs.readdirSync(this.config.dataDir) .filter(file => file.endsWith('.json')); for (const file of dataFiles) { const dbName = file.replace('.json', ''); const data = this._getDatabase(dbName); const collections = Object.keys(data || {}); totalCollections += collections.length; databaseStats[dbName] = { collections: collections.length, collectionsNames: collections, size: JSON.stringify(data || {}).length }; } } const stats = { totalCollections, databases: databaseStats, dataDir: this.config.dataDir, serverStarted: this.serverStartTime, connectedClients: this.clients.size }; collections.forEach(collection => { const data = this.data[collection]; stats.collections[collection] = { keys: Object.keys(data).length, size: JSON.stringify(data).length }; }); res.json(stats); } catch (error) { console.error('Stats Error:', error); res.status(500).json({ error: error.message }); } } // ========== BACKUP SYSTEM ========== /** * Otomatik backup sistemi (24 saatte bir) */ _setupAutoBackup() { // Backup dizinini oluştur if (!fs.existsSync(this.config.backupDir)) { fs.mkdirSync(this.config.backupDir, { recursive: true }); } // 24 saatte bir backup al this.backupInterval = setInterval(() => { this._createBackup('auto'); this._cleanupOldBackups(); }, 24 * 60 * 60 * 1000); // 24 hours console.log('CloudLoyalDB: Otomatik backup sistemi aktif (24 saatte bir)'); // İlk startup backup - sadece 10 dakika içinde backup yoksa this._createStartupBackupIfNeeded(); } /** * Startup backup - sadece son 10 dakika içinde backup yoksa oluştur */ _createStartupBackupIfNeeded() { try { // Backup klasörünü kontrol et if (!fs.existsSync(this.config.backupDir)) { fs.mkdirSync(this.config.backupDir, { recursive: true }); this._createBackup('startup'); return; } // Son 10 dakika içindeki backup'ları kontrol et const backupFiles = fs.readdirSync(this.config.backupDir); const tenMinutesAgo = Date.now() - (10 * 60 * 1000); // 10 dakika let recentBackupFound = false; let recentBackupFile = ''; for (const file of backupFiles) { const filePath = path.join(this.config.backupDir, file); const stats = fs.statSync(filePath); if (stats.mtimeMs > tenMinutesAgo) { recentBackupFound = true; recentBackupFile = file; break; } } if (recentBackupFound) { console.log('CloudLoyalDB: Son 10 dakika içinde backup mevcut, yeni backup oluşturulmuyor:', recentBackupFile); } else { console.log('CloudLoyalDB: Son 10 dakika içinde backup bulunamadı, startup backup oluşturuluyor...'); this._createBackup('startup'); } } catch (error) { console.error('CloudLoyalDB: Startup backup kontrolü hatası:', error.message); // Hata durumunda backup oluştur this._createBackup('startup'); } } /** * Backup oluştur */ _createBackup(type = 'manual', database = 'all') { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); if (database === 'all') { // Tüm database'leri backup et if (!fs.existsSync(this.config.dataDir)) { console.log('CloudLoyalDB: data/ klasörü bulunamadı, boş backup oluşturuluyor'); return { success: true, backups: [] }; } const dataFiles = fs.readdirSync(this.config.dataDir) .filter(file => file.endsWith('.json')); if (dataFiles.length === 0) { console.log('CloudLoyalDB: data/ klasöründe JSON dosyası bulunamadı'); return { success: true, backups: [] }; } const allBackups = []; for (const file of dataFiles) { const dbName = file.replace('.json', ''); const singleBackup = this._createSingleBackup(type, dbName, timestamp); allBackups.push(singleBackup); } console.log(`CloudLoyalDB: ${allBackups.length} database backup'ı oluşturuldu`); return { success: true, backups: allBackups }; } else { // Tek database backup'ı return this._createSingleBackup(type, database, timestamp); } } catch (error) { console.error('Backup oluşturma hatası:', error); throw error; } } _createSingleBackup(type, database, timestamp) { const backupFileName = `${database}-backup-${type}-${timestamp}.json`; const backupPath = path.join(this.config.backupDir, backupFileName); const data = this._getDatabase(database); const backupData = { metadata: { type, database, createdAt: new Date().toISOString(), originalFile: this.getDataFile(database), totalCollections: Object.keys(data || {}).length, serverVersion: '1.0.0' }, data: data || {} }; fs.writeFileSync(backupPath, JSON.stringify(backupData, null, 2), 'utf8'); console.log(`CloudLoyalDB: ${database}.json backup'ı oluşturuldu: ${backupFileName}`); // WebSocket ile bildir this._broadcast({ type: 'backup_completed', database, backupId: backupFileName, timestamp: Date.now(), backupType: type }); return { success: true, backupFile: backupFileName, database }; } /** * 2 günden eski backup'ları sil */ _cleanupOldBackups() { try { const files = fs.readdirSync(this.config.backupDir); const twoDaysAgo = Date.now() - (2 * 24 * 60 * 60 * 1000); // 2 days in milliseconds let deletedCount = 0; files.forEach(file => { if (file.startsWith('flydb-backup-') && file.endsWith('.json')) { const filePath = path.join(this.config.backupDir, file); const stats = fs.statSync(filePath); if (stats.mtime.getTime() < twoDaysAgo) { fs.unlinkSync(filePath); deletedCount++; console.log(`CloudLoyalDB: Eski backup silindi: ${file}`); } } }); if (deletedCount > 0) { console.log(`CloudLoyalDB: ${deletedCount} eski backup temizlendi (2+ gün)`); } } catch (error) { console.error('Backup cleanup hatası:', error); } } /** * Manuel backup endpoint */ _handleManualBackup(req, res) { try { const backupFileName = this._createBackup('manual'); res.json({ success: true, backupFile: backupFileName, path: path.join(this.config.backupDir, backupFileName) }); } catch (error) { res.status(500).json({ error: error.message }); } } /** * Backup listesi */ _handleListBackups(req, res) { try { const files = fs.readdirSync(this.config.backupDir); const backups = files .filter(file => file.startsWith('flydb-backup-') && file.endsWith('.json')) .map(file => { const filePath = path.join(this.config.backupDir, file); const stats = fs.statSync(filePath); return { filename: file, size: stats.size, createdAt: stats.mtime.toISOString(), age: Date.now() - stats.mtime.getTime() }; }) .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); res.json({ backups }); } catch (error) { res.status(500).json({ error: error.message }); } } // ========== HELPER METHODS ========== /** * Nested value setter */ _setNestedValue(obj, path, value) { const keys = path.split('.'); const lastKey = keys.pop(); let current = obj; for (const key of keys) { if (!current[key] || typeof current[key] !== 'object' || Array.isArray(current[key])) { current[key] = {}; } current = current[key]; } current[lastKey] = value; } /** * Nested value getter */ _getNestedValue(obj, path) { if (!path) return obj; const keys = path.split('.'); let current = obj; for (const key of keys) { if (current === null || current === undefined || typeof current !== 'object') { return undefined; } current = current[key]; } return current; } /** * Server'ı başlat */ async start() { try { this.serverStartTime = new Date().toISOString(); this.app.listen(this.config.port, () => { console.log(`CloudLoyalDB Server çalışıyor:`); console.log(` HTTP API: http://localhost:${this.config.port}/api`); console.log(` WebSocket: ws://localhost:${this.config.wsPort}`); console.log(` Data Dir: ${this.config.dataDir}`); console.log(` Backup Dir: ${this.config.backupDir}`); // Database bilgilerini göster if (fs.existsSync(this.config.dataDir)) { const dataFiles = fs.readdirSync(this.config.dataDir) .filter(file => file.endsWith('.json')); console.log(` Databases: ${dataFiles.length}`); dataFiles.forEach(file => { const dbName = file.replace('.json', ''); const data = this._getDatabase(dbName); const collections = Object.keys(data || {}).length; console.log(` - ${dbName}: ${collections} collections`); }); } else { console.log(` Databases: 0 (data/ klasörü bulunamadı)`); } }); } catch (error) { console.error('Server başlatma hatası:', error); process.exit(1); } } /** * Server'ı durdur */ stop() { if (this.backupInterval) { clearInterval(this.backupInterval); } if (this.wss) { this.wss.close(); } console.log('CloudLoyalDB Server durduruldu'); } /** * UNPUSH - Array'den değer silme */ _handleUnpush(req, res) { try { const { collection, path, value } = req.body; if (!collection || !path) { return res.status(400).json({ error: 'Collection ve path gereklidir' }); } if (!this.data[collection]) { return res.status(404).json({ error: 'Collection bulunamadı' }); } let arr = this._getNestedValue(this.data[collection], path); if (Array.isArray(arr)) { const index = arr.indexOf(value); if (index !== -1) { arr.splice(index, 1); this._setNestedValue(this.data[collection], path, arr); this._saveData(); } } this._broadcast({ type: 'data_changed', collection, path, value, operation: 'unpush', timestamp: Date.now() }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } } /** * HAS - Veri kontrolü */ _handleHas(req, res) { try { const { collection, path } = req.query; if (!collection) { return res.status(400).json({ error: 'Collection gereklidir' }); } if (!this.data[collection]) { return res.json({ exists: false }); } const value = path ? this._getNestedValue(this.data[collection], path) : this.data[collection]; res.json({ exists: value !== undefined }); } catch (error) { res.status(500).json({ error: error.message }); } } /** * DELETE - Veri silme */ _handleDelete(req, res) { try { const { collection, path } = req.body; if (!collection || !path) { return res.status(400).json({ error: 'Collection ve path gereklidir' }); } if (!this.data[collection]) { return res.status(404).json({ error: 'Collection bulunamadı' }); } const deleted = this._deleteNestedValue(this.data[collection], path); if (deleted) { this._saveData(); this._broadcast({ type: 'data_changed', collection, path, operation: 'delete', timestamp: Date.now() }); } res.json({ success: deleted }); } catch (error) { res.status(500).json({ error: error.message }); } } /** * DELETEALL - Koleksiyon temizleme */ _handleDeleteAll(req, res) { try { const { collection } = req.body; if (!collection) { return res.status(400).json({ error: 'Collection gereklidir' }); } this.data[collection] = {}; this._saveData(); this._broadcast({ type: 'collection_cleared', collection, timestamp: Date.now() }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } } /** * DELBYPRIORITY - Index ile array elemanı silme */ _handleDelByPriority(req, res) { try { const { collection, path, index } = req.body; if (!collection || !path || typeof index !== 'number') { return res.status(400).json({ error: 'Collection, path ve index gereklidir' }); } if (!this.data[collection]) { return res.status(404).json({ error: 'Collection bulunamadı' }); } let arr = this._getNestedValue(this.data[collection], path); if (Array.isArray(arr) && index >= 0 && index < arr.length) { arr.splice(index, 1); this._setNestedValue(this.data[collection], path, arr); this._saveData(); } this._broadcast({ type: 'data_changed', collection, path, index, operation: 'delByPriority', timestamp: Date.now() }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } } /** * SETBYPRIORITY - Index ile array elemanı güncelleme */ _handleSetByPriority(req, res) { try { const { collection, path, value, index } = req.body; if (!collection || !path || typeof index !== 'number') { return res.status(400).json({ error: 'Collection, path, value ve index gereklidir' }); } if (!this.data[collection]) { this.data[collection] = {}; } let arr = this._getNestedValue(this.data[collection], path); if (!Array.isArray(arr)) { arr = []; } // Array'i genişlet (gerekirse) while (arr.length <= index) { arr.push(null); } arr[index] = value; this._setNestedValue(this.data[collection], path, arr); this._saveData(); this._broadcast({ type: 'data_changed', collection, path, value, index, operation: 'setByPriority', timestamp: Date.now() }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } } /** * RESTORE - Backup geri yükleme */ _handleRestoreBackup(req, res) { try { const { backupFileName } = req.body; if (!backupFileName) { return res.status(400).json({ error: 'Backup dosya adı gereklidir' }); } const backupPath = path.join(this.config.backupDir, backupFileName); if (!fs.existsSync(backupPath)) { return res.status(404).json({ error: 'Backup dosyası bulunamadı' }); } // Güvenlik backup'ı al this._createBackup('safety-restore'); // Backup'ı yükle const backupString = fs.readFileSync(backupPath, 'utf8'); const backupData = JSON.parse(backupString); this.data = backupData.data; this._saveData(); console.log('CloudLoyalDB: Backup geri yüklendi:', backupFileName); this._broadcast({ type: 'backup_restored', backupFileName, timestamp: Date.now() }); res.json({ success: true, restoredFrom: backupData.metadata, restoredAt: new Date().toISOString() }); } catch (error) { res.status(500).json({ error: error.message }); } } /** * Nested value operations helper */ _deleteNestedValue(obj, path) { if (!path || !obj) return false; const keys = path.split('.'); const lastKey = keys.pop(); let current = obj; for (const key of keys) { if (!current[key] || typeof current[key] !== 'object') { return false; } current = current[key]; } if (current && Object.prototype.hasOwnProperty.call(current, lastKey)) { delete current[lastKey]; return true; } return false; } } // Server'ı başlat if (require.main === module) { const server = new CloudLoyalDBServer({ port: process.env.PORT || 3000, wsPort: process.env.WS_PORT || 3001, corsOrigin: process.env.CORS_ORIGIN || '*' }); server.start(); // Graceful shutdown process.on('SIGINT', () => { console.log('\nCloudLoyalDB Server kapatılıyor...'); server.stop(); process.exit(0); }); } module.exports = CloudLoyalDBServer;