load the database file when encryption is off (#1137)

This commit is contained in:
ZacharyZcR
2026-07-29 18:38:10 +08:00
committed by GitHub
parent e3205ec7ed
commit 32fb7487df
2 changed files with 108 additions and 2 deletions
+28 -2
View File
@@ -156,8 +156,34 @@ async function initializeDatabaseAsync(): Promise<void> {
}
} else {
assertDataDirIsNotMisconfigured(dataDir);
memoryDatabase = new Database(":memory:");
isNewDatabase = true;
// The database still lives in memory and is serialised out on every write;
// turning encryption off only changes whether that file is ciphertext. It
// has to be read back, or each restart starts empty and silently discards
// everything the previous run saved.
const existing = readPlainDatabaseFile();
if (existing) {
memoryDatabase = new Database(existing);
databaseLogger.info("Loaded unencrypted database from disk", {
operation: "db_load_plain",
path: dbPath,
bytes: existing.length,
});
} else {
memoryDatabase = new Database(":memory:");
isNewDatabase = true;
}
}
}
/** The plain database file, or null when there is nothing to restore. */
function readPlainDatabaseFile(): Buffer | null {
try {
const contents = fs.readFileSync(dbPath);
return contents.length > 0 ? contents : null;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
}
}
@@ -0,0 +1,80 @@
import fs from "fs";
import os from "os";
import path from "path";
import Database from "better-sqlite3";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
/**
* `DB_FILE_ENCRYPTION=false` used to mean "start empty, every time".
*
* The database lives in memory on every backend and is serialised to disk after
* writes; the flag only decides whether that file is ciphertext. The plain
* branch wrote `db.sqlite` faithfully and then never read it back, so each
* restart began with an empty database and silently discarded everything the
* previous run had saved. The data-dir guard made it worse by confirming a
* database was present in DATA_DIR immediately before it was thrown away.
*/
describe("unencrypted database persistence", () => {
let dataDir: string;
beforeEach(() => {
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-plain-db-"));
vi.resetModules();
process.env.DATA_DIR = dataDir;
process.env.DB_FILE_ENCRYPTION = "false";
process.env.ALLOW_EMPTY_DATA_DIR = "true";
});
afterEach(() => {
delete process.env.DATA_DIR;
delete process.env.DB_FILE_ENCRYPTION;
delete process.env.ALLOW_EMPTY_DATA_DIR;
fs.rmSync(dataDir, { recursive: true, force: true });
});
/** A database file with one row, as a previous run would have left it. */
function writeExistingDatabase(): void {
const seed = new Database(":memory:");
seed.exec("CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT)");
seed
.prepare("INSERT INTO settings (key, value) VALUES (?, ?)")
.run("survives_restart", "yes");
fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize());
seed.close();
}
it("reads back what an earlier run wrote", async () => {
writeExistingDatabase();
const db = await import("../../../database/db/index.js");
await db.initializeDatabase();
const row = db
.getSqlite()
.prepare("SELECT value FROM settings WHERE key = ?")
.get("survives_restart") as { value: string } | undefined;
expect(row?.value).toBe("yes");
});
it("starts empty when there is no file yet", async () => {
const db = await import("../../../database/db/index.js");
await db.initializeDatabase();
// Startup creates its own tables; the point is that it does not throw on a
// missing file and does not carry rows over from nowhere.
const row = db
.getSqlite()
.prepare("SELECT COUNT(*) AS count FROM users")
.get() as { count: number };
expect(row.count).toBe(0);
});
it("ignores a zero-length file rather than failing to open it", async () => {
fs.writeFileSync(path.join(dataDir, "db.sqlite"), "");
const db = await import("../../../database/db/index.js");
await expect(db.initializeDatabase()).resolves.not.toThrow();
});
});