663 lines
19 KiB
TypeScript
663 lines
19 KiB
TypeScript
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
import Database from "better-sqlite3";
|
|
import * as schema from "./schema.js";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { databaseLogger } from "../../utils/logger.js";
|
|
import { DatabaseFileEncryption } from "../../utils/database-file-encryption.js";
|
|
|
|
const dataDir = process.env.DATA_DIR || "./db/data";
|
|
const dbDir = path.resolve(dataDir);
|
|
if (!fs.existsSync(dbDir)) {
|
|
databaseLogger.info(`Creating database directory`, {
|
|
operation: "db_init",
|
|
path: dbDir,
|
|
});
|
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
}
|
|
|
|
// Database file encryption configuration
|
|
const enableFileEncryption = process.env.DB_FILE_ENCRYPTION !== "false";
|
|
const dbPath = path.join(dataDir, "db.sqlite");
|
|
const encryptedDbPath = `${dbPath}.encrypted`;
|
|
|
|
// Initialize database with file encryption support
|
|
let actualDbPath = ":memory:"; // Always use memory database
|
|
let memoryDatabase: Database.Database;
|
|
let isNewDatabase = false;
|
|
|
|
if (enableFileEncryption) {
|
|
try {
|
|
// Check if encrypted database exists
|
|
if (DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath)) {
|
|
databaseLogger.info(
|
|
"Found encrypted database file, loading into memory...",
|
|
{
|
|
operation: "db_memory_load",
|
|
encryptedPath: encryptedDbPath,
|
|
},
|
|
);
|
|
|
|
// Validate hardware compatibility
|
|
if (
|
|
!DatabaseFileEncryption.validateHardwareCompatibility(encryptedDbPath)
|
|
) {
|
|
databaseLogger.error(
|
|
"Hardware fingerprint mismatch for encrypted database",
|
|
{
|
|
operation: "db_decrypt_failed",
|
|
reason: "hardware_mismatch",
|
|
},
|
|
);
|
|
throw new Error(
|
|
"Cannot decrypt database: hardware fingerprint mismatch",
|
|
);
|
|
}
|
|
|
|
// Decrypt database content to memory buffer
|
|
const decryptedBuffer =
|
|
DatabaseFileEncryption.decryptDatabaseToBuffer(encryptedDbPath);
|
|
|
|
// Create in-memory database from decrypted buffer
|
|
memoryDatabase = new Database(decryptedBuffer);
|
|
} else {
|
|
memoryDatabase = new Database(":memory:");
|
|
isNewDatabase = true;
|
|
|
|
// Check if there's an old unencrypted database to migrate
|
|
if (fs.existsSync(dbPath)) {
|
|
// Load old database and copy its content to memory database
|
|
const oldDb = new Database(dbPath, { readonly: true });
|
|
|
|
// Get all table schemas and data from old database
|
|
const tables = oldDb
|
|
.prepare(
|
|
`
|
|
SELECT name, sql FROM sqlite_master
|
|
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
`,
|
|
)
|
|
.all() as { name: string; sql: string }[];
|
|
|
|
// Create tables in memory database
|
|
for (const table of tables) {
|
|
memoryDatabase.exec(table.sql);
|
|
}
|
|
|
|
// Copy data for each table
|
|
for (const table of tables) {
|
|
const rows = oldDb.prepare(`SELECT * FROM ${table.name}`).all();
|
|
if (rows.length > 0) {
|
|
const columns = Object.keys(rows[0]);
|
|
const placeholders = columns.map(() => "?").join(", ");
|
|
const insertStmt = memoryDatabase.prepare(
|
|
`INSERT INTO ${table.name} (${columns.join(", ")}) VALUES (${placeholders})`,
|
|
);
|
|
|
|
for (const row of rows) {
|
|
const values = columns.map((col) => (row as any)[col]);
|
|
insertStmt.run(values);
|
|
}
|
|
}
|
|
}
|
|
|
|
oldDb.close();
|
|
|
|
isNewDatabase = false;
|
|
} else {
|
|
}
|
|
}
|
|
} catch (error) {
|
|
databaseLogger.error("Failed to initialize memory database", error, {
|
|
operation: "db_memory_init_failed",
|
|
});
|
|
|
|
// If file encryption is critical, fail fast
|
|
if (process.env.DB_FILE_ENCRYPTION_REQUIRED === "true") {
|
|
throw error;
|
|
}
|
|
|
|
memoryDatabase = new Database(":memory:");
|
|
isNewDatabase = true;
|
|
}
|
|
} else {
|
|
memoryDatabase = new Database(":memory:");
|
|
isNewDatabase = true;
|
|
}
|
|
|
|
databaseLogger.info(`Initializing SQLite database`, {
|
|
operation: "db_init",
|
|
path: actualDbPath,
|
|
encrypted:
|
|
enableFileEncryption &&
|
|
DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath),
|
|
inMemory: true,
|
|
isNewDatabase,
|
|
});
|
|
|
|
const sqlite = memoryDatabase;
|
|
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
username TEXT NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
|
is_oidc INTEGER NOT NULL DEFAULT 0,
|
|
client_id TEXT NOT NULL,
|
|
client_secret TEXT NOT NULL,
|
|
issuer_url TEXT NOT NULL,
|
|
authorization_url TEXT NOT NULL,
|
|
token_url TEXT NOT NULL,
|
|
redirect_uri TEXT,
|
|
identifier_path TEXT NOT NULL,
|
|
name_path TEXT NOT NULL,
|
|
scopes TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ssh_data (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
name TEXT,
|
|
ip TEXT NOT NULL,
|
|
port INTEGER NOT NULL,
|
|
username TEXT NOT NULL,
|
|
folder TEXT,
|
|
tags TEXT,
|
|
pin INTEGER NOT NULL DEFAULT 0,
|
|
auth_type TEXT NOT NULL,
|
|
password TEXT,
|
|
key TEXT,
|
|
key_password TEXT,
|
|
key_type TEXT,
|
|
enable_terminal INTEGER NOT NULL DEFAULT 1,
|
|
enable_tunnel INTEGER NOT NULL DEFAULT 1,
|
|
tunnel_connections TEXT,
|
|
enable_file_manager INTEGER NOT NULL DEFAULT 1,
|
|
default_path TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS file_manager_recent (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
host_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
last_opened TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id),
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS file_manager_pinned (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
host_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
pinned_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id),
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS file_manager_shortcuts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
host_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id),
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS dismissed_alerts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
alert_id TEXT NOT NULL,
|
|
dismissed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ssh_credentials (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
folder TEXT,
|
|
tags TEXT,
|
|
auth_type TEXT NOT NULL,
|
|
username TEXT NOT NULL,
|
|
password TEXT,
|
|
key TEXT,
|
|
key_password TEXT,
|
|
key_type TEXT,
|
|
usage_count INTEGER NOT NULL DEFAULT 0,
|
|
last_used TEXT,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ssh_credential_usage (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
credential_id INTEGER NOT NULL,
|
|
host_id INTEGER NOT NULL,
|
|
user_id TEXT NOT NULL,
|
|
used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (credential_id) REFERENCES ssh_credentials (id),
|
|
FOREIGN KEY (host_id) REFERENCES ssh_data (id),
|
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
);
|
|
`);
|
|
|
|
const addColumnIfNotExists = (
|
|
table: string,
|
|
column: string,
|
|
definition: string,
|
|
) => {
|
|
try {
|
|
sqlite
|
|
.prepare(
|
|
`SELECT ${column}
|
|
FROM ${table} LIMIT 1`,
|
|
)
|
|
.get();
|
|
} catch (e) {
|
|
try {
|
|
databaseLogger.debug(`Adding column ${column} to ${table}`, {
|
|
operation: "schema_migration",
|
|
table,
|
|
column,
|
|
});
|
|
sqlite.exec(`ALTER TABLE ${table}
|
|
ADD COLUMN ${column} ${definition};`);
|
|
databaseLogger.success(`Column ${column} added to ${table}`, {
|
|
operation: "schema_migration",
|
|
table,
|
|
column,
|
|
});
|
|
} catch (alterError) {
|
|
databaseLogger.warn(`Failed to add column ${column} to ${table}`, {
|
|
operation: "schema_migration",
|
|
table,
|
|
column,
|
|
error: alterError,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const migrateSchema = () => {
|
|
databaseLogger.info("Checking for schema updates...", {
|
|
operation: "schema_migration",
|
|
});
|
|
|
|
addColumnIfNotExists("users", "is_admin", "INTEGER NOT NULL DEFAULT 0");
|
|
|
|
addColumnIfNotExists("users", "is_oidc", "INTEGER NOT NULL DEFAULT 0");
|
|
addColumnIfNotExists("users", "oidc_identifier", "TEXT");
|
|
addColumnIfNotExists("users", "client_id", "TEXT");
|
|
addColumnIfNotExists("users", "client_secret", "TEXT");
|
|
addColumnIfNotExists("users", "issuer_url", "TEXT");
|
|
addColumnIfNotExists("users", "authorization_url", "TEXT");
|
|
addColumnIfNotExists("users", "token_url", "TEXT");
|
|
|
|
addColumnIfNotExists("users", "identifier_path", "TEXT");
|
|
addColumnIfNotExists("users", "name_path", "TEXT");
|
|
addColumnIfNotExists("users", "scopes", "TEXT");
|
|
|
|
addColumnIfNotExists("users", "totp_secret", "TEXT");
|
|
addColumnIfNotExists("users", "totp_enabled", "INTEGER NOT NULL DEFAULT 0");
|
|
addColumnIfNotExists("users", "totp_backup_codes", "TEXT");
|
|
|
|
addColumnIfNotExists("ssh_data", "name", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "folder", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "tags", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "pin", "INTEGER NOT NULL DEFAULT 0");
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"auth_type",
|
|
'TEXT NOT NULL DEFAULT "password"',
|
|
);
|
|
addColumnIfNotExists("ssh_data", "password", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "key", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "key_password", "TEXT");
|
|
addColumnIfNotExists("ssh_data", "key_type", "TEXT");
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"enable_terminal",
|
|
"INTEGER NOT NULL DEFAULT 1",
|
|
);
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"enable_tunnel",
|
|
"INTEGER NOT NULL DEFAULT 1",
|
|
);
|
|
addColumnIfNotExists("ssh_data", "tunnel_connections", "TEXT");
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"enable_file_manager",
|
|
"INTEGER NOT NULL DEFAULT 1",
|
|
);
|
|
addColumnIfNotExists("ssh_data", "default_path", "TEXT");
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"created_at",
|
|
"TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
);
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"updated_at",
|
|
"TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
);
|
|
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"credential_id",
|
|
"INTEGER REFERENCES ssh_credentials(id)",
|
|
);
|
|
|
|
addColumnIfNotExists(
|
|
"ssh_data",
|
|
"require_password",
|
|
"INTEGER NOT NULL DEFAULT 1",
|
|
);
|
|
|
|
// SSH credentials table migrations for encryption support
|
|
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
|
|
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
|
|
addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT");
|
|
|
|
addColumnIfNotExists("file_manager_recent", "host_id", "INTEGER NOT NULL");
|
|
addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL");
|
|
addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL");
|
|
|
|
databaseLogger.success("Schema migration completed", {
|
|
operation: "schema_migration",
|
|
});
|
|
};
|
|
|
|
const initializeDatabase = async (): Promise<void> => {
|
|
migrateSchema();
|
|
|
|
try {
|
|
const row = sqlite
|
|
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
|
|
.get();
|
|
if (!row) {
|
|
databaseLogger.info("Initializing default settings", {
|
|
operation: "db_init",
|
|
setting: "allow_registration",
|
|
});
|
|
sqlite
|
|
.prepare(
|
|
"INSERT INTO settings (key, value) VALUES ('allow_registration', 'true')",
|
|
)
|
|
.run();
|
|
} else {
|
|
}
|
|
} catch (e) {
|
|
databaseLogger.warn("Could not initialize default settings", {
|
|
operation: "db_init",
|
|
error: e,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Function to save in-memory database to encrypted file
|
|
async function saveMemoryDatabaseToFile() {
|
|
if (!memoryDatabase || !enableFileEncryption) return;
|
|
|
|
try {
|
|
// Export in-memory database to buffer
|
|
const buffer = memoryDatabase.serialize();
|
|
|
|
// Encrypt and save to file
|
|
DatabaseFileEncryption.encryptDatabaseFromBuffer(buffer, encryptedDbPath);
|
|
|
|
databaseLogger.debug("In-memory database saved to encrypted file", {
|
|
operation: "memory_db_save",
|
|
bufferSize: buffer.length,
|
|
encryptedPath: encryptedDbPath,
|
|
});
|
|
} catch (error) {
|
|
databaseLogger.error("Failed to save in-memory database", error, {
|
|
operation: "memory_db_save_failed",
|
|
});
|
|
}
|
|
}
|
|
|
|
// Function to handle post-initialization file encryption and cleanup
|
|
async function handlePostInitFileEncryption() {
|
|
if (!enableFileEncryption) return;
|
|
|
|
try {
|
|
// Clean up any existing unencrypted database files
|
|
if (fs.existsSync(dbPath)) {
|
|
databaseLogger.warn(
|
|
"Found unencrypted database file, removing for security",
|
|
{
|
|
operation: "db_security_cleanup_existing",
|
|
removingPath: dbPath,
|
|
},
|
|
);
|
|
|
|
try {
|
|
fs.unlinkSync(dbPath);
|
|
databaseLogger.success(
|
|
"Unencrypted database file removed for security",
|
|
{
|
|
operation: "db_security_cleanup_complete",
|
|
removedPath: dbPath,
|
|
},
|
|
);
|
|
} catch (error) {
|
|
databaseLogger.warn(
|
|
"Could not remove unencrypted database file (may be locked)",
|
|
{
|
|
operation: "db_security_cleanup_deferred",
|
|
path: dbPath,
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
},
|
|
);
|
|
|
|
// Try again after a short delay
|
|
setTimeout(() => {
|
|
try {
|
|
if (fs.existsSync(dbPath)) {
|
|
fs.unlinkSync(dbPath);
|
|
databaseLogger.success(
|
|
"Delayed cleanup: unencrypted database file removed",
|
|
{
|
|
operation: "db_security_cleanup_delayed_success",
|
|
removedPath: dbPath,
|
|
},
|
|
);
|
|
}
|
|
} catch (delayedError) {
|
|
databaseLogger.error(
|
|
"Failed to remove unencrypted database file even after delay",
|
|
delayedError,
|
|
{
|
|
operation: "db_security_cleanup_delayed_failed",
|
|
path: dbPath,
|
|
},
|
|
);
|
|
}
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
// Always save the in-memory database (whether new or existing)
|
|
if (memoryDatabase) {
|
|
// Save immediately after initialization
|
|
await saveMemoryDatabaseToFile();
|
|
|
|
// Set up periodic saves every 5 minutes
|
|
setInterval(saveMemoryDatabaseToFile, 5 * 60 * 1000);
|
|
}
|
|
} catch (error) {
|
|
databaseLogger.error(
|
|
"Failed to handle database file encryption/cleanup",
|
|
error,
|
|
{
|
|
operation: "db_encrypt_cleanup_failed",
|
|
},
|
|
);
|
|
|
|
// Don't fail the entire initialization for this
|
|
}
|
|
}
|
|
|
|
initializeDatabase()
|
|
.then(() => handlePostInitFileEncryption())
|
|
.catch((error) => {
|
|
databaseLogger.error("Failed to initialize database", error, {
|
|
operation: "db_init",
|
|
});
|
|
process.exit(1);
|
|
});
|
|
|
|
databaseLogger.success("Database connection established", {
|
|
operation: "db_init",
|
|
path: actualDbPath,
|
|
hasEncryptedBackup:
|
|
enableFileEncryption &&
|
|
DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath),
|
|
});
|
|
|
|
// Cleanup function for database and temporary files
|
|
async function cleanupDatabase() {
|
|
// Save in-memory database before closing
|
|
if (memoryDatabase) {
|
|
try {
|
|
await saveMemoryDatabaseToFile();
|
|
} catch (error) {
|
|
databaseLogger.error(
|
|
"Failed to save in-memory database before shutdown",
|
|
error,
|
|
{
|
|
operation: "shutdown_save_failed",
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// Close database connection
|
|
try {
|
|
if (sqlite) {
|
|
sqlite.close();
|
|
databaseLogger.debug("Database connection closed", {
|
|
operation: "db_close",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
databaseLogger.warn("Error closing database connection", {
|
|
operation: "db_close_error",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
|
|
// Clean up temp directory
|
|
try {
|
|
const tempDir = path.join(dataDir, ".temp");
|
|
if (fs.existsSync(tempDir)) {
|
|
const files = fs.readdirSync(tempDir);
|
|
for (const file of files) {
|
|
try {
|
|
fs.unlinkSync(path.join(tempDir, file));
|
|
} catch {
|
|
// Ignore individual file cleanup errors
|
|
}
|
|
}
|
|
|
|
try {
|
|
fs.rmdirSync(tempDir);
|
|
databaseLogger.debug("Temp directory cleaned up", {
|
|
operation: "temp_dir_cleanup",
|
|
});
|
|
} catch {
|
|
// Ignore directory removal errors
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Ignore temp directory cleanup errors
|
|
}
|
|
}
|
|
|
|
// Register cleanup handlers
|
|
process.on("exit", () => {
|
|
// Synchronous cleanup only for exit event
|
|
if (sqlite) {
|
|
try {
|
|
sqlite.close();
|
|
} catch {}
|
|
}
|
|
});
|
|
|
|
process.on("SIGINT", async () => {
|
|
databaseLogger.info("Received SIGINT, cleaning up...", {
|
|
operation: "shutdown",
|
|
});
|
|
await cleanupDatabase();
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on("SIGTERM", async () => {
|
|
databaseLogger.info("Received SIGTERM, cleaning up...", {
|
|
operation: "shutdown",
|
|
});
|
|
await cleanupDatabase();
|
|
process.exit(0);
|
|
});
|
|
|
|
// Export database connection and file encryption utilities
|
|
export const db = drizzle(sqlite, { schema });
|
|
export const sqliteInstance = sqlite; // Export underlying SQLite instance for schema queries
|
|
export { DatabaseFileEncryption };
|
|
export const databasePaths = {
|
|
main: actualDbPath,
|
|
encrypted: encryptedDbPath,
|
|
directory: dbDir,
|
|
inMemory: true,
|
|
};
|
|
|
|
// Memory database buffer function
|
|
function getMemoryDatabaseBuffer(): Buffer {
|
|
if (!memoryDatabase) {
|
|
throw new Error("Memory database not initialized");
|
|
}
|
|
|
|
try {
|
|
// Export in-memory database to buffer
|
|
const buffer = memoryDatabase.serialize();
|
|
|
|
databaseLogger.debug("Memory database serialized to buffer", {
|
|
operation: "memory_db_serialize",
|
|
bufferSize: buffer.length,
|
|
});
|
|
|
|
return buffer;
|
|
} catch (error) {
|
|
databaseLogger.error(
|
|
"Failed to serialize memory database to buffer",
|
|
error,
|
|
{
|
|
operation: "memory_db_serialize_failed",
|
|
},
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Export save function for manual saves and buffer access
|
|
export { saveMemoryDatabaseToFile, getMemoryDatabaseBuffer };
|