evelodb
Version:
A high-performance native B-tree database for Node.js. Made by Evelocore.
292 lines (291 loc) • 12.2 kB
JavaScript
"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;