commit 6ae996e78eba29c4aaf19a347dccddf8003441ee
parent 51d902402de1356a9089d9ab867d340dcaf83315
Author: Florian Dold <florian@dold.me>
Date: Fri, 20 Jun 2025 22:03:32 +0200
wallet-core: implement importDbFromFile
Diffstat:
6 files changed, 144 insertions(+), 17 deletions(-)
diff --git a/packages/taler-util/src/qtart.ts b/packages/taler-util/src/qtart.ts
@@ -20,15 +20,27 @@ export interface ErrReceiver {
errno?: number;
}
+/**
+ * File handle. Thin wrapper around a libc FILE object.
+ */
export interface QjsFile {
/**
* Close the file. Return 0 if OK or -errno in case of I/O error.
*/
close(): number;
+
/**
* Outputs the string with UTF-8 encoding.
*/
puts(str: string): void;
+
+ /**
+ * Read max_size bytes from the file and return them as a string
+ * assuming UTF-8 encoding.
+ *
+ * If max_size is not present, the file is read up its end.
+ */
+ readAsString(max_size?: number): string;
}
export interface QjsOsLib {
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
@@ -4135,3 +4135,16 @@ export interface ExportDbToFileResponse {
*/
path: string;
}
+
+export interface ImportDbFromFileRequest {
+ /**
+ * Full path to the backup.
+ */
+ path: string;
+}
+
+export const codecForImportDbFromFileRequest =
+ (): Codec<ImportDbFromFileRequest> =>
+ buildCodecForObject<ImportDbFromFileRequest>()
+ .property("path", codecForString())
+ .build("ImportDbFromFileRequest");
diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts
@@ -29,9 +29,7 @@ import {
createSqliteBackend,
shimIndexedDB,
} from "@gnu-taler/idb-bridge";
-import {
- createNodeHelperSqlite3Impl,
-} from "@gnu-taler/idb-bridge/node-helper-sqlite3-impl";
+import { createNodeHelperSqlite3Impl } from "@gnu-taler/idb-bridge/node-helper-sqlite3-impl";
import {
Logger,
SetTimeoutTimerAPI,
@@ -104,6 +102,9 @@ async function makeFileDb(
exportToFile(directory, stem) {
throw Error("not supported");
},
+ async readBackupJson(path: string): Promise<any> {
+ throw Error("not supported");
+ },
};
}
@@ -143,6 +144,9 @@ async function makeSqliteDb(
path,
};
},
+ async readBackupJson(path: string): Promise<any> {
+ throw Error("not supported");
+ },
idbFactory: myBridgeIdbFactory,
};
}
diff --git a/packages/taler-wallet-core/src/host-impl.qtart.ts b/packages/taler-wallet-core/src/host-impl.qtart.ts
@@ -29,7 +29,6 @@ import type {
} from "@gnu-taler/idb-bridge";
// eslint-disable-next-line no-duplicate-imports
import {
- AccessStats,
BridgeIDBFactory,
createSqliteBackend,
MemoryBackend,
@@ -54,11 +53,6 @@ import { Wallet, WalletDatabaseImplementation } from "./wallet.js";
const logger = new Logger("host-impl.qtart.ts");
-interface MakeDbResult {
- idbFactory: BridgeIDBFactory;
- getStats: () => AccessStats;
-}
-
let numStmt = 0;
export async function createQtartSqlite3Impl(): Promise<Sqlite3Interface> {
@@ -147,6 +141,20 @@ async function makeSqliteDb(
throw Error(`forcing format ${forceFormat} not supported`);
}
},
+ async readBackupJson(path: string): Promise<any> {
+ const errObj = { errno: undefined };
+ const file = qjsStd.open(path, "r", errObj);
+ if (!path.endsWith(".json")) {
+ throw Error("DB file import only supports .json files at the moment");
+ }
+ if (!file) {
+ throw Error(`could not open file (errno=${errObj.errno})`);
+ }
+ const dumpStr = file.readAsString();
+ file.close();
+ const dump = JSON.parse(dumpStr);
+ return dump;
+ },
idbFactory: myBridgeIdbFactory,
};
}
@@ -192,6 +200,9 @@ async function makeFileDb(
exportToFile(directory, stem) {
throw Error("not supported");
},
+ async readBackupJson(path: string): Promise<any> {
+ throw Error("not supported");
+ },
};
}
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -106,6 +106,7 @@ import {
GetWithdrawalDetailsForAmountRequest,
GetWithdrawalDetailsForUriRequest,
HintNetworkAvailabilityRequest,
+ ImportDbFromFileRequest,
ImportDbRequest,
InitRequest,
InitResponse,
@@ -255,6 +256,7 @@ export enum WalletApiOperation {
ImportDb = "importDb",
ExportDb = "exportDb",
ExportDbToFile = "exportDbToFile",
+ ImportDbFromFile = "importDbFromFile",
PreparePeerPushCredit = "preparePeerPushCredit",
CheckPeerPushDebit = "checkPeerPushDebit",
CheckPeerPushDebitV2 = "checkPeerPushDebitV2",
@@ -946,6 +948,17 @@ export type ExportDbToFileOp = {
};
/**
+ * Export the database from a file.
+ *
+ * CAUTION: Overrides existing data.
+ */
+export type ImportDbFromFileOp = {
+ op: WalletApiOperation.ImportDbFromFile;
+ request: ImportDbFromFileRequest;
+ response: EmptyObject;
+};
+
+/**
* Add a new backup provider.
*/
export type AddBackupProviderOp = {
@@ -1440,6 +1453,7 @@ export type WalletOperations = {
[WalletApiOperation.RunBackupCycle]: RunBackupCycleOp;
[WalletApiOperation.ExportBackup]: ExportBackupOp;
[WalletApiOperation.ExportDbToFile]: ExportDbToFileOp;
+ [WalletApiOperation.ImportDbFromFile]: ImportDbFromFileOp;
[WalletApiOperation.AddBackupProvider]: AddBackupProviderOp;
[WalletApiOperation.RemoveBackupProvider]: RemoveBackupProviderOp;
[WalletApiOperation.GetBackupInfo]: GetBackupInfoOp;
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -45,6 +45,7 @@ import {
AmountJson,
AmountString,
Amounts,
+ AsyncCondition,
CancellationToken,
CanonicalizeBaseUrlRequest,
CanonicalizeBaseUrlResponse,
@@ -83,6 +84,8 @@ import {
GetQrCodesForPaytoRequest,
GetQrCodesForPaytoResponse,
HintNetworkAvailabilityRequest,
+ ImportDbFromFileRequest,
+ ImportDbRequest,
InitRequest,
InitResponse,
IntegrationTestArgs,
@@ -185,6 +188,7 @@ import {
codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri,
codecForHintNetworkAvailabilityRequest,
+ codecForImportDbFromFileRequest,
codecForImportDbRequest,
codecForInitRequest,
codecForInitiatePeerPullPaymentRequest,
@@ -1677,6 +1681,32 @@ async function handleExportDbToFile(
};
}
+async function handleImportDb(
+ wex: WalletExecutionContext,
+ req: ImportDbRequest,
+): Promise<EmptyObject> {
+ // FIXME: This should atomically re-materialize transactions!
+ await importDb(wex.db.idbHandle(), req.dump);
+ await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
+ await rematerializeTransactions(wex, tx);
+ });
+ return {};
+}
+
+async function handleImportDbFromFile(
+ wex: WalletExecutionContext,
+ req: ImportDbFromFileRequest,
+): Promise<EmptyObject> {
+ if (req.path.endsWith(".json")) {
+ const dump = await wex.ws.dbImplementation.readBackupJson(req.path);
+ return await handleImportDb(wex, {
+ dump,
+ });
+ } else {
+ throw Error("DB file import only supports .json files at the moment");
+ }
+}
+
async function handleAcceptBankIntegratedWithdrawal(
wex: WalletExecutionContext,
req: AcceptBankIntegratedWithdrawalRequest,
@@ -2241,14 +2271,11 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = {
},
[WalletApiOperation.ImportDb]: {
codec: codecForImportDbRequest(),
- handler: async (wex, req) => {
- // FIXME: This should atomically re-materialize transactions!
- await importDb(wex.db.idbHandle(), req.dump);
- await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
- await rematerializeTransactions(wex, tx);
- });
- return {};
- },
+ handler: handleImportDb,
+ },
+ [WalletApiOperation.ImportDbFromFile]: {
+ codec: codecForImportDbFromFileRequest(),
+ handler: handleImportDbFromFile,
},
[WalletApiOperation.CheckPeerPushDebit]: {
codec: codecForCheckPeerPushDebitRequest(),
@@ -2563,6 +2590,7 @@ export interface WalletDatabaseImplementation {
stem: string,
forceFormat?: string,
) => Promise<{ path: string }>;
+ readBackupJson(path: string): Promise<any>;
}
/**
@@ -2782,6 +2810,10 @@ export class InternalWalletState {
longpollQueue = new LongpollQueue();
+ private loadingDb: boolean = false;
+
+ private loadingDbCond: AsyncCondition = new AsyncCondition();
+
public get idbFactory(): BridgeIDBFactory {
return this.dbImplementation.idbFactory;
}
@@ -2877,6 +2909,12 @@ export class InternalWalletState {
if (this._indexedDbHandle) {
return;
}
+ if (this.loadingDb) {
+ while (this.loadingDb) {
+ await this.loadingDbCond.wait();
+ }
+ }
+ this.loadingDb = true;
const myVersionChange = async (): Promise<void> => {
logger.info("version change requested for Taler DB");
};
@@ -2890,9 +2928,44 @@ export class InternalWalletState {
throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, {
innerError: getErrorDetailFromException(e),
});
+ } finally {
+ this.loadingDb = false;
+ this.loadingDbCond.trigger();
}
}
+ /**
+ * Prepare database for import by closing it.
+ */
+ async suspendDatabase(): Promise<void> {
+ if (this.loadingDb) {
+ while (this.loadingDb) {
+ await this.loadingDbCond.wait();
+ }
+ }
+ this.loadingDb = true;
+ const dbh = this._indexedDbHandle;
+ if (!dbh) {
+ return;
+ }
+ this._indexedDbHandle = undefined;
+ return new Promise((resolve, reject) => {
+ dbh.addEventListener("close", () => {
+ resolve();
+ });
+ dbh.close();
+ });
+ }
+
+ /**
+ * Resume database by re-opening it.
+ */
+ async resumeDatabase(): Promise<void> {
+ this.loadingDb = false;
+ this.loadingDbCond.trigger();
+ await this.ensureWalletDbOpen();
+ }
+
notify(n: WalletNotification): void {
logger.trace(`Notification: ${j2s(n)}`);
for (const l of this.listeners) {