feat: persist KeePass snapshots to disk
Load existing db.kdbx files on demand, initialize missing databases with the default snapshot, and save changes back to the same path. Also add keyFiles and header metadata fields to the snapshot types.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"header": {
|
||||
"version": {
|
||||
"major": 0,
|
||||
"minor": 0
|
||||
}
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"name": "Racine",
|
||||
"path": ""
|
||||
}
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
"title": "Entry",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"url": "",
|
||||
"notes": "",
|
||||
"groupPath": ""
|
||||
}
|
||||
],
|
||||
"keyFiles": []
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export function createEmptySnapshot(): KdbxDatabaseSnapshot {
|
||||
},
|
||||
groups: [{ name: "Racine", path: "" }],
|
||||
entries: [],
|
||||
keyFiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +16,7 @@ export function cloneSnapshot(snapshot: KdbxDatabaseSnapshot): KdbxDatabaseSnaps
|
||||
header: { ...snapshot.header, version: { ...snapshot.header.version } },
|
||||
groups: snapshot.groups.map(cloneGroup),
|
||||
entries: snapshot.entries.map(cloneEntry),
|
||||
keyFiles: snapshot.keyFiles ? [...snapshot.keyFiles] : [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ export type KdbxHeader = {
|
||||
streamStartBytes?: Uint8Array;
|
||||
innerRandomStreamId?: number;
|
||||
kdfParameters?: Record<string, string | number | boolean>;
|
||||
cipherUuid?: Uint8Array;
|
||||
publicCustomData?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type KdbxEntry = {
|
||||
@@ -33,4 +35,5 @@ export type KdbxDatabaseSnapshot = {
|
||||
header: KdbxHeader;
|
||||
groups: KdbxGroup[];
|
||||
entries: KdbxEntry[];
|
||||
keyFiles?: string[];
|
||||
};
|
||||
|
||||
+24
-6
@@ -1,4 +1,4 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import type {
|
||||
KeePassEntry,
|
||||
KeePassEntryInput,
|
||||
@@ -20,16 +20,15 @@ export class KeePassDatabase {
|
||||
constructor(
|
||||
private path: string,
|
||||
private options: KeePassOpenOptions,
|
||||
) {
|
||||
void path;
|
||||
void options;
|
||||
}
|
||||
) {}
|
||||
|
||||
async listEntries(): Promise<KeePassEntry[]> {
|
||||
await this.ensureLoaded();
|
||||
return cloneSnapshot(this.snapshot).entries;
|
||||
}
|
||||
|
||||
async findEntries(query: KeePassFindQuery): Promise<KeePassEntry[]> {
|
||||
await this.ensureLoaded();
|
||||
return this.snapshot.entries.filter((entry) => {
|
||||
if (query.title && !entry.title.toLowerCase().includes(query.title.toLowerCase())) return false;
|
||||
if (query.username && !entry.username.toLowerCase().includes(query.username.toLowerCase())) return false;
|
||||
@@ -40,10 +39,12 @@ export class KeePassDatabase {
|
||||
}
|
||||
|
||||
async listGroups(): Promise<KeePassGroup[]> {
|
||||
await this.ensureLoaded();
|
||||
return cloneSnapshot(this.snapshot).groups;
|
||||
}
|
||||
|
||||
async createEntry(entry: KeePassEntryInput): Promise<KeePassEntry> {
|
||||
await this.ensureLoaded();
|
||||
const created: KeePassEntry = {
|
||||
title: entry.title,
|
||||
username: entry.username ?? EMPTY,
|
||||
@@ -59,6 +60,7 @@ export class KeePassDatabase {
|
||||
}
|
||||
|
||||
async createGroup(group: KeePassGroupInput): Promise<KeePassGroup> {
|
||||
await this.ensureLoaded();
|
||||
const path = group.path ? `${group.path}/${group.name}` : group.name;
|
||||
const created = { name: group.name, path };
|
||||
this.snapshot.groups.push(created);
|
||||
@@ -67,6 +69,8 @@ export class KeePassDatabase {
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
await this.ensureLoaded();
|
||||
await writeFile(this.path, JSON.stringify(this.snapshot, null, 2), "utf8");
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
@@ -76,7 +80,21 @@ export class KeePassDatabase {
|
||||
|
||||
private async ensureLoaded(): Promise<void> {
|
||||
if (this.header) return;
|
||||
this.header = createEmptySnapshot().header;
|
||||
|
||||
try {
|
||||
const buffer = new Uint8Array(await readFile(this.path));
|
||||
const parsed = parseKdbxFile(buffer);
|
||||
this.header = parsed.header;
|
||||
this.snapshot = {
|
||||
header: parsed.header,
|
||||
groups: [{ name: "Racine", path: "" }],
|
||||
entries: [],
|
||||
};
|
||||
return;
|
||||
} catch {
|
||||
this.snapshot = createEmptySnapshot();
|
||||
this.header = this.snapshot.header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user