UNPKG

evelodb

Version:

A high-performance native B-tree database for Node.js. Made by Evelocore.

292 lines (291 loc) 12.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.BackupManager = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); const bson_1 = require("bson"); /** * Handles all backup and restoration logic for EveloDB */ class BackupManager { db; constructor(db) { this.db = db; } createBackup(collection, config) { const type = config.type || 'binary'; if (!collection || !config.path) return { success: false, err: 'Invalid request' }; try { if (!fs.existsSync(config.path)) fs.mkdirSync(config.path, { recursive: true }); const now = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `${collection}_backup_${now}`; const schema = this.db.config.schema?.[collection] || {}; const serializedSchema = this.serializeSchema(schema); if (type === 'json') { const recordsRes = this.db.allInternal(collection); if (!Array.isArray(recordsRes)) return { success: false, err: 'Failed to retrieve records' }; const backupData = { collection, schema: { [collection]: serializedSchema }, length: recordsRes.length, created: new Date(), data: recordsRes }; const fullPath = path.join(config.path, `${filename}.json`); fs.writeFileSync(fullPath, JSON.stringify(backupData, null, 2)); return { success: true, backupPath: fullPath }; } else if (type === 'binary') { const recordsRes = this.db.allInternal(collection); if (!Array.isArray(recordsRes)) return { success: false, err: 'Failed to retrieve records' }; const records = recordsRes; const backupData = { title: config.title || '', collection, protected: !!config.password, schema: { [collection]: serializedSchema }, length: records.length, created: new Date(), data: records }; let fileBuffer = Buffer.from(bson_1.BSON.serialize(backupData)); if (config.password) { fileBuffer = this.encrypt(fileBuffer, config.password); } const fullPath = path.join(config.path, `${filename}.backup`); fs.writeFileSync(fullPath, fileBuffer); return { success: true, backupPath: fullPath }; } else { return { success: false, err: 'Invalid backup type' }; } } catch (e) { return { success: false, err: e.message }; } } restoreBackup(collection, config) { const type = config.type || 'binary'; const filePath = config.file; if (!collection || !filePath || !fs.existsSync(filePath)) return { success: false, err: 'Invalid request or file not found' }; try { this.db.closeHandle(collection); const { dataPath } = this.db.getBsonPaths(collection); if (type === 'json') { const backup = JSON.parse(fs.readFileSync(filePath, 'utf-8')); this.applyRestoredSchema(collection, backup.schema); this.db.drop(collection); for (const record of backup.data || []) this.db.create(collection, record); return { success: true }; } else if (type === 'binary') { const backupInfo = this.readBackupFile(filePath, config.password); if (!backupInfo.success) return { success: false, err: backupInfo.err }; // If it was protected, we MUST have records (unless length was 0) if (backupInfo.protected && (backupInfo.length || 0) > 0 && (!backupInfo.data || backupInfo.data.length === 0)) { return { success: false, err: 'Invalid password or decryption failed' }; } this.applyRestoredSchema(collection, backupInfo.schema); this.db.drop(collection); for (const record of backupInfo.data) this.db.create(collection, record); return { success: true }; } else { return { success: false, err: 'Invalid backup type' }; } } catch (e) { return { success: false, err: e.message }; } } readBackupFile(filePath, password) { if (!fs.existsSync(filePath)) return { success: false, err: 'File not found', data: [] }; try { const isBinary = filePath.endsWith('.backup'); if (isBinary) { let fileBuffer = fs.readFileSync(filePath); let backup; // 1. Try reading as plain BSON first try { backup = bson_1.BSON.deserialize(fileBuffer); // If we got here, the file was NOT whole-file encrypted } catch (e) { // 2. If BSON fails, it MUST be whole-file encrypted if (!password) return { success: false, err: 'Backup is encrypted. Password required.', data: [] }; try { fileBuffer = this.decrypt(fileBuffer, password); backup = bson_1.BSON.deserialize(fileBuffer); } catch (decryptErr) { return { success: false, err: 'Invalid password or corrupted backup file.', data: [] }; } } // 3. Check protection status vs password if (backup.protected && !password) { // Allow reading metadata but return no records if password missing return { success: true, title: backup.title, protected: true, schema: backup.schema, length: backup.length, data: [], created: backup.created }; } return { success: true, title: backup.title, protected: !!backup.protected, schema: backup.schema, length: backup.length, data: backup.data || [], created: backup.created }; } else { const backup = JSON.parse(fs.readFileSync(filePath, 'utf-8')); return { success: true, title: backup.collection, protected: false, schema: backup.schema, length: backup.data?.length || 0, data: backup.data || [], created: new Date() }; } } catch (e) { return { success: false, err: e.message, data: [] }; } } serializeSchema(schema) { if (!schema) return {}; const serializeFields = (fields) => { if (!fields) return undefined; const result = {}; for (const [k, v] of Object.entries(fields)) { const cfg = { ...v }; if (cfg.type === String) cfg.type = 'String'; else if (cfg.type === Number) cfg.type = 'Number'; else if (cfg.type === Boolean) cfg.type = 'Boolean'; else if (cfg.type === Array) cfg.type = 'Array'; else if (cfg.type === Object) cfg.type = 'Object'; else if (typeof cfg.type === 'object' && cfg.type !== null) cfg.type = serializeFields(cfg.type); result[k] = cfg; } return result; }; return { fields: serializeFields(schema.fields), indexes: schema.indexes, uniqueKeys: schema.uniqueKeys, objectIdKey: schema.objectIdKey, noRepeat: schema.noRepeat }; } applyRestoredSchema(collection, schema) { if (schema) { // Handle both old (direct) and new (wrapped) schema formats const targetSchema = schema[collection] || schema; const deserializeFields = (fields) => { if (!fields) return undefined; const result = {}; for (const [k, v] of Object.entries(fields)) { const cfg = { ...v }; if (cfg.type === 'String') cfg.type = String; else if (cfg.type === 'Number') cfg.type = Number; else if (cfg.type === 'Boolean') cfg.type = Boolean; else if (cfg.type === 'Array') cfg.type = Array; else if (cfg.type === 'Object') cfg.type = Object; else if (typeof cfg.type === 'object' && cfg.type !== null) cfg.type = deserializeFields(cfg.type); result[k] = cfg; } return result; }; if (!this.db.config.schema) this.db.config.schema = {}; this.db.config.schema[collection] = { fields: deserializeFields(targetSchema.fields), indexes: targetSchema.indexes, uniqueKeys: targetSchema.uniqueKeys, objectIdKey: targetSchema.objectIdKey, noRepeat: targetSchema.noRepeat }; } } encrypt(buffer, password) { const key = crypto.createHash('sha256').update(password).digest(); const result = Buffer.alloc(buffer.length); for (let i = 0; i < buffer.length; i++) { result[i] = buffer[i] ^ key[i % key.length]; } return result; } decrypt(buffer, password) { // XOR is its own inverse return this.encrypt(buffer, password); } } exports.BackupManager = BackupManager;