From 64e78d03a117fffeb18e18154d9028a2532285a5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 1 Sep 2023 10:52:15 +0200 Subject: wallet-core: implement and test stored backups --- packages/taler-wallet-core/src/db.ts | 53 +++++++++++------- packages/taler-wallet-core/src/host-impl.node.ts | 6 ++- packages/taler-wallet-core/src/wallet.ts | 68 ++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 24 deletions(-) (limited to 'packages/taler-wallet-core') diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index a642c0203..b9d86eb25 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -22,6 +22,7 @@ import { IDBDatabase, IDBFactory, IDBObjectStore, + IDBRequest, IDBTransaction, structuredEncapsulate, } from "@gnu-taler/idb-bridge"; @@ -59,6 +60,7 @@ import { Logger, CoinPublicKeyString, TalerPreciseTimestamp, + j2s, } from "@gnu-taler/taler-util"; import { DbAccess, @@ -117,7 +119,8 @@ export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta"; /** * Stored backups, mainly created when manually importing a backup. */ -export const TALER_WALLET_STORED_BACKUPS_DB_NAME = "taler-wallet-stored-backups"; +export const TALER_WALLET_STORED_BACKUPS_DB_NAME = + "taler-wallet-stored-backups"; export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; @@ -2833,11 +2836,10 @@ export async function exportSingleDb( dbName, undefined, () => { - // May not happen, since we're not requesting a specific version - throw Error("unexpected version change"); + logger.info(`unexpected onversionchange in exportSingleDb of ${dbName}`); }, () => { - logger.info("unexpected onupgradeneeded"); + logger.info(`unexpected onupgradeneeded in exportSingleDb of ${dbName}`); }, ); @@ -2849,7 +2851,7 @@ export async function exportSingleDb( return new Promise((resolve, reject) => { const tx = myDb.transaction(Array.from(myDb.objectStoreNames)); tx.addEventListener("complete", () => { - myDb.close(); + //myDb.close(); resolve(singleDbDump); }); // tslint:disable-next-line:prefer-for-of @@ -2885,6 +2887,7 @@ export async function exportSingleDb( if (store.keyPath == null) { rec.key = structuredEncapsulate(cursor.key); } + storeDump.records.push(rec); cursor.continue(); } }); @@ -2913,21 +2916,22 @@ async function recoverFromDump( db: IDBDatabase, dbDump: DbDumpDatabase, ): Promise { - return new Promise((resolve, reject) => { - const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); - tx.addEventListener("complete", () => { - resolve(); - }); - for (let i = 0; i < db.objectStoreNames.length; i++) { - const name = db.objectStoreNames[i]; - const storeDump = dbDump.stores[name]; - if (!storeDump) continue; - for (let rec of storeDump.records) { - tx.objectStore(name).put(rec.value, rec.key); - } + const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); + const txProm = promiseFromTransaction(tx); + const storeNames = db.objectStoreNames; + for (let i = 0; i < storeNames.length; i++) { + const name = db.objectStoreNames[i]; + const storeDump = dbDump.stores[name]; + if (!storeDump) continue; + await promiseFromRequest(tx.objectStore(name).clear()); + logger.info(`importing ${storeDump.records.length} records into ${name}`); + for (let rec of storeDump.records) { + await promiseFromRequest(tx.objectStore(name).put(rec.value, rec.key)); + logger.info("importing record done"); } - tx.commit(); - }); + } + tx.commit(); + return await txProm; } function checkDbDump(x: any): x is DbDump { @@ -3184,6 +3188,17 @@ function promiseFromTransaction(transaction: IDBTransaction): Promise { }); } +export function promiseFromRequest(request: IDBRequest): Promise { + return new Promise((resolve, reject) => { + request.onsuccess = () => { + resolve(request.result); + }; + request.onerror = () => { + reject(request.error); + }; + }); +} + /** * Purge all data in the given database. */ diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts index 0b6539306..0626b9254 100644 --- a/packages/taler-wallet-core/src/host-impl.node.ts +++ b/packages/taler-wallet-core/src/host-impl.node.ts @@ -52,7 +52,6 @@ interface MakeDbResult { async function makeFileDb( args: DefaultNodeWalletArgs = {}, ): Promise { - BridgeIDBFactory.enableTracing = false; const myBackend = new MemoryBackend(); myBackend.enableTracing = false; const storagePath = args.persistentStoragePath; @@ -141,7 +140,10 @@ export async function createNativeWalletHost2( let dbResp: MakeDbResult; - if (args.persistentStoragePath &&args.persistentStoragePath.endsWith(".json")) { + if ( + args.persistentStoragePath && + args.persistentStoragePath.endsWith(".json") + ) { logger.info("using legacy file-based DB backend"); dbResp = await makeFileDb(args); } else { diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 626409dd6..5666d67e0 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -121,6 +121,11 @@ import { GetCurrencyInfoResponse, codecForGetCurrencyInfoRequest, CreateStoredBackupResponse, + StoredBackupList, + codecForDeleteStoredBackupRequest, + DeleteStoredBackupRequest, + RecoverStoredBackupRequest, + codecForRecoverStoredBackupRequest, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -1041,6 +1046,57 @@ async function createStoredBackup( }; } +async function listStoredBackups( + ws: InternalWalletState, +): Promise { + const storedBackups: StoredBackupList = { + storedBackups: [], + }; + const backupsDb = await openStoredBackupsDatabase(ws.idb); + await backupsDb.mktxAll().runReadWrite(async (tx) => { + await tx.backupMeta.iter().forEach((x) => { + storedBackups.storedBackups.push({ + name: x.name, + }); + }); + }); + return storedBackups; +} + +async function deleteStoredBackup( + ws: InternalWalletState, + req: DeleteStoredBackupRequest, +): Promise { + const backupsDb = await openStoredBackupsDatabase(ws.idb); + await backupsDb.mktxAll().runReadWrite(async (tx) => { + await tx.backupData.delete(req.name); + await tx.backupMeta.delete(req.name); + }); +} + +async function recoverStoredBackup( + ws: InternalWalletState, + req: RecoverStoredBackupRequest, +): Promise { + logger.info(`Recovering stored backup ${req.name}`); + const { name } = req; + const backupsDb = await openStoredBackupsDatabase(ws.idb); + const bd = await backupsDb.mktxAll().runReadWrite(async (tx) => { + const backupMeta = tx.backupMeta.get(name); + if (!backupMeta) { + throw Error("backup not found"); + } + const backupData = await tx.backupData.get(name); + if (!backupData) { + throw Error("no backup data (DB corrupt)"); + } + return backupData; + }); + logger.info(`backup found, now importing`); + await importDb(ws.db.idbHandle(), bd); + logger.info(`import done`); +} + /** * Implementation of the "wallet-core" API. */ @@ -1059,12 +1115,18 @@ async function dispatchRequestInternal( switch (operation) { case WalletApiOperation.CreateStoredBackup: return createStoredBackup(ws); - case WalletApiOperation.DeleteStoredBackup: + case WalletApiOperation.DeleteStoredBackup: { + const req = codecForDeleteStoredBackupRequest().decode(payload); + await deleteStoredBackup(ws, req); return {}; + } case WalletApiOperation.ListStoredBackups: + return listStoredBackups(ws); + case WalletApiOperation.RecoverStoredBackup: { + const req = codecForRecoverStoredBackupRequest().decode(payload); + await recoverStoredBackup(ws, req); return {}; - case WalletApiOperation.RecoverStoredBackup: - return {}; + } case WalletApiOperation.InitWallet: { logger.trace("initializing wallet"); ws.initCalled = true; -- cgit v1.2.3