@nocobase/plugin-backups 2.2.0-beta.5 → 2.2.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/components/RestoreFromBackup.d.ts +8 -0
- package/dist/client/components/RestoreFromLocal.d.ts +8 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/567.942d36050408575f.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +12 -11
- package/dist/node_modules/archiver/package.json +1 -1
- package/dist/node_modules/cron-parser/package.json +1 -1
- package/dist/node_modules/fs-extra/package.json +1 -1
- package/dist/node_modules/semver/package.json +1 -1
- package/dist/node_modules/yauzl/package.json +1 -1
- package/dist/server/adapters/database.d.ts +7 -0
- package/dist/server/adapters/database.js +253 -33
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +7 -1
- package/dist/server/managers/backup.d.ts +4 -0
- package/dist/server/managers/backup.js +7 -2
- package/dist/server/managers/restore.d.ts +1 -0
- package/dist/server/managers/restore.js +27 -4
- package/dist/server/utils.d.ts +1 -0
- package/dist/server/utils.js +29 -1
- package/package.json +2 -2
- package/dist/client-v2/567.d39a82a6aa6452a0.js +0 -10
|
@@ -10,6 +10,9 @@ import { DatabaseOptions } from '@nocobase/database';
|
|
|
10
10
|
export type DBBackupOptions = {
|
|
11
11
|
dir: string;
|
|
12
12
|
skipFdw?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated Prefer excludeTables. includeTables may miss dependent database objects.
|
|
15
|
+
*/
|
|
13
16
|
includeTables?: string[];
|
|
14
17
|
excludeTables?: string[];
|
|
15
18
|
};
|
|
@@ -17,9 +20,13 @@ export type DBRestoreOptions = {
|
|
|
17
20
|
filePath: string;
|
|
18
21
|
schema?: string;
|
|
19
22
|
skipDropAllTables?: boolean;
|
|
23
|
+
restoreMode?: 'preserveTables';
|
|
24
|
+
toolchain?: DBBackupToolchain;
|
|
20
25
|
};
|
|
26
|
+
export type DBBackupToolchain = 'postgres' | 'kingbase';
|
|
21
27
|
export interface DBAdapter {
|
|
22
28
|
dbOpts: DatabaseOptions;
|
|
29
|
+
backupToolchain?: DBBackupToolchain;
|
|
23
30
|
backup(options: DBBackupOptions): Promise<void>;
|
|
24
31
|
restore(options: DBRestoreOptions): Promise<void>;
|
|
25
32
|
check(op: 'backup' | 'restore'): Promise<void>;
|
|
@@ -52,8 +52,15 @@ const D$$ = import_os.default.platform() === "win32" ? "$$" : "\\$\\$";
|
|
|
52
52
|
const STREAM_BUFFER_SIZE = 2 * 1024 * 1024;
|
|
53
53
|
const run = async (command, envVars = {}) => {
|
|
54
54
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
55
|
+
const result = await exec(command, { env: { ...process.env, ...envVars } });
|
|
56
|
+
if (typeof result === "string") {
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
if (result && typeof result === "object" && "stdout" in result) {
|
|
60
|
+
const { stdout } = result;
|
|
61
|
+
return typeof stdout === "string" ? stdout : String(stdout ?? "");
|
|
62
|
+
}
|
|
63
|
+
return "";
|
|
57
64
|
} catch (error) {
|
|
58
65
|
throw new Error(`${error.message}`);
|
|
59
66
|
}
|
|
@@ -68,6 +75,20 @@ const escapeStringLiteral = (value) => String(value).replace(/'/g, "''");
|
|
|
68
75
|
const quotePgIdentifier = (value) => `"${String(value).replace(/"/g, '""')}"`;
|
|
69
76
|
const quoteShellArg = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
70
77
|
const quotePgTablePattern = (table) => quoteShellArg(String(table).split(".").map(quotePgIdentifier).join("."));
|
|
78
|
+
const isPgRestoreSchemaTocEntry = (line) => /^\d+;\s+\d+\s+\d+\s+SCHEMA\s+-\s+/.test(line);
|
|
79
|
+
const parsePgTableReference = (table, defaultSchema) => {
|
|
80
|
+
const parts = String(table).split(".");
|
|
81
|
+
if (parts.length > 1) {
|
|
82
|
+
return {
|
|
83
|
+
schema: parts[0],
|
|
84
|
+
table: parts.slice(1).join(".")
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
schema: defaultSchema,
|
|
89
|
+
table: parts[0]
|
|
90
|
+
};
|
|
91
|
+
};
|
|
71
92
|
const qualifyPgTablePattern = (table, schema) => {
|
|
72
93
|
const tablePattern = String(table);
|
|
73
94
|
if (!schema || tablePattern.includes(".")) {
|
|
@@ -97,6 +118,14 @@ class BaseDBAdapter {
|
|
|
97
118
|
);
|
|
98
119
|
}
|
|
99
120
|
};
|
|
121
|
+
hasCommand(command) {
|
|
122
|
+
try {
|
|
123
|
+
(0, import_child_process.execSync)(`${command} --version`);
|
|
124
|
+
return true;
|
|
125
|
+
} catch (_error) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
100
129
|
}
|
|
101
130
|
class MySQLAdapter extends BaseDBAdapter {
|
|
102
131
|
#backupCmd = "mysqldump";
|
|
@@ -145,6 +174,7 @@ class MySQLAdapter extends BaseDBAdapter {
|
|
|
145
174
|
"--set-gtid-purged=OFF",
|
|
146
175
|
"--routines",
|
|
147
176
|
"--triggers",
|
|
177
|
+
"--events",
|
|
148
178
|
...version && version > 7 ? ["--column-statistics=0"] : [],
|
|
149
179
|
database
|
|
150
180
|
];
|
|
@@ -237,9 +267,47 @@ class MySQLAdapter extends BaseDBAdapter {
|
|
|
237
267
|
return "";
|
|
238
268
|
}
|
|
239
269
|
}
|
|
240
|
-
async restore({ filePath, skipDropAllTables = false }) {
|
|
270
|
+
async restore({ filePath, skipDropAllTables = false, restoreMode }) {
|
|
241
271
|
const { username, host, port, database, password } = this.dbOpts;
|
|
242
|
-
if (
|
|
272
|
+
if (restoreMode === "preserveTables") {
|
|
273
|
+
const dropViewsCommand = `mysql -u ${username} -h ${host} ${port ? `-P ${port}` : ""} --protocol=tcp -D ${database} -e "
|
|
274
|
+
DELIMITER $$
|
|
275
|
+
DROP PROCEDURE IF EXISTS drop_all_views$$
|
|
276
|
+
CREATE PROCEDURE drop_all_views()
|
|
277
|
+
BEGIN
|
|
278
|
+
DECLARE _done INT DEFAULT FALSE;
|
|
279
|
+
DECLARE _viewName VARCHAR(255);
|
|
280
|
+
|
|
281
|
+
DECLARE _cursor CURSOR FOR
|
|
282
|
+
SELECT table_name
|
|
283
|
+
FROM information_schema.VIEWS
|
|
284
|
+
WHERE table_schema = SCHEMA();
|
|
285
|
+
|
|
286
|
+
DECLARE CONTINUE HANDLER FOR NOT FOUND SET _done = TRUE;
|
|
287
|
+
|
|
288
|
+
OPEN _cursor;
|
|
289
|
+
|
|
290
|
+
REPEAT
|
|
291
|
+
FETCH _cursor INTO _viewName;
|
|
292
|
+
|
|
293
|
+
IF NOT _done THEN
|
|
294
|
+
SET @stmt_sql = CONCAT('DROP VIEW IF EXISTS ', _viewName);
|
|
295
|
+
PREPARE stmt FROM @stmt_sql;
|
|
296
|
+
EXECUTE stmt;
|
|
297
|
+
DEALLOCATE PREPARE stmt;
|
|
298
|
+
END IF;
|
|
299
|
+
|
|
300
|
+
UNTIL _done END REPEAT;
|
|
301
|
+
|
|
302
|
+
CLOSE _cursor;
|
|
303
|
+
END$$
|
|
304
|
+
|
|
305
|
+
CALL drop_all_views()$$
|
|
306
|
+
DROP PROCEDURE drop_all_views$$
|
|
307
|
+
DELIMITER ;
|
|
308
|
+
"`;
|
|
309
|
+
await run(dropViewsCommand, { MYSQL_PWD: password });
|
|
310
|
+
} else if (!skipDropAllTables) {
|
|
243
311
|
const dropDataCommand = `mysql -u ${username} -h ${host} ${port ? `-P ${port}` : ""} --protocol=tcp -D ${database} -e "
|
|
244
312
|
DELIMITER $$
|
|
245
313
|
DROP PROCEDURE IF EXISTS drop_all_tables_and_triggers$$
|
|
@@ -327,24 +395,36 @@ class MySQLAdapter extends BaseDBAdapter {
|
|
|
327
395
|
}
|
|
328
396
|
}
|
|
329
397
|
class PostgresAdapter extends BaseDBAdapter {
|
|
330
|
-
get
|
|
398
|
+
get backupToolchain() {
|
|
399
|
+
return "postgres";
|
|
400
|
+
}
|
|
401
|
+
getBackupCommandName(_toolchain = this.backupToolchain) {
|
|
331
402
|
return formatPathInEnv(process.env.PG_DUMP_PATH) || "pg_dump";
|
|
332
403
|
}
|
|
333
|
-
|
|
404
|
+
getRestoreCommandName(_toolchain = this.backupToolchain) {
|
|
334
405
|
return formatPathInEnv(process.env.PG_RESTORE_PATH) || "pg_restore";
|
|
335
406
|
}
|
|
407
|
+
getSqlCommandName(_toolchain = this.backupToolchain) {
|
|
408
|
+
return formatPathInEnv(process.env.PSQL_PATH) || "psql";
|
|
409
|
+
}
|
|
410
|
+
getPasswordEnvVars(password, _toolchain = this.backupToolchain) {
|
|
411
|
+
return { PGPASSWORD: password };
|
|
412
|
+
}
|
|
413
|
+
getBackupSchema() {
|
|
414
|
+
return this.dbOpts.schema;
|
|
415
|
+
}
|
|
336
416
|
async check(op) {
|
|
337
417
|
switch (op) {
|
|
338
418
|
case "backup":
|
|
339
|
-
this.assertCommand(this
|
|
419
|
+
this.assertCommand(this.getBackupCommandName());
|
|
340
420
|
break;
|
|
341
421
|
case "restore":
|
|
342
|
-
this.assertCommand(this
|
|
422
|
+
this.assertCommand(this.getRestoreCommandName());
|
|
343
423
|
break;
|
|
344
424
|
}
|
|
345
425
|
}
|
|
346
426
|
async clientVersion(op) {
|
|
347
|
-
const cmd = op === "backup" ? this
|
|
427
|
+
const cmd = op === "backup" ? this.getBackupCommandName() : this.getRestoreCommandName();
|
|
348
428
|
try {
|
|
349
429
|
return (0, import_child_process.execSync)(`${cmd} --version`).toString();
|
|
350
430
|
} catch (_error) {
|
|
@@ -352,15 +432,53 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
352
432
|
}
|
|
353
433
|
}
|
|
354
434
|
async backup({ dir, includeTables, excludeTables }) {
|
|
355
|
-
const { username, host, port, database, password
|
|
435
|
+
const { username, host, port, database, password } = this.dbOpts;
|
|
356
436
|
const filePath = `${dir}/data`;
|
|
437
|
+
const backupSchema = this.getBackupSchema();
|
|
357
438
|
const schemaOption = backupSchema ? `--schema=${backupSchema}` : "";
|
|
439
|
+
const expandedExcludeTables = Array.isArray(excludeTables) ? [.../* @__PURE__ */ new Set([...excludeTables, ...await this.getOwnedSequenceTables(excludeTables, backupSchema)])] : [];
|
|
358
440
|
const includeOption = Array.isArray(includeTables) && includeTables.length ? includeTables.map((table) => `-t ${quotePgTablePattern(qualifyPgTablePattern(table, backupSchema))}`).join(" ") : "";
|
|
359
|
-
const excludeOption =
|
|
360
|
-
const command = `${this
|
|
361
|
-
await run(command,
|
|
441
|
+
const excludeOption = expandedExcludeTables.length ? expandedExcludeTables.map((table) => `-T ${quotePgTablePattern(qualifyPgTablePattern(table, backupSchema))}`).join(" ") : "";
|
|
442
|
+
const command = `${this.getBackupCommandName()} ${includeOption} ${excludeOption} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -F c -b --quote-all-identifiers ${schemaOption} -f ${filePath} ${database}`;
|
|
443
|
+
await run(command, this.getPasswordEnvVars(password));
|
|
444
|
+
}
|
|
445
|
+
async getOwnedSequenceTables(excludeTables, backupSchema) {
|
|
446
|
+
if (!Array.isArray(excludeTables) || !excludeTables.length) {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
const { username, host, port, database, password } = this.dbOpts;
|
|
450
|
+
const tableRefs = excludeTables.map((table) => parsePgTableReference(table, backupSchema)).filter((ref) => ref.table);
|
|
451
|
+
if (!tableRefs.length) {
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
const values = tableRefs.map((ref) => {
|
|
455
|
+
const schemaValue = ref.schema == null ? "NULL" : `'${escapeStringLiteral(ref.schema)}'`;
|
|
456
|
+
return `(${schemaValue}, '${escapeStringLiteral(ref.table)}')`;
|
|
457
|
+
}).join(",");
|
|
458
|
+
const query = `
|
|
459
|
+
WITH excluded(schema_name, table_name) AS (VALUES ${values})
|
|
460
|
+
SELECT seq_ns.nspname || '.' || seq.relname
|
|
461
|
+
FROM excluded
|
|
462
|
+
JOIN pg_class tbl ON tbl.relname = excluded.table_name
|
|
463
|
+
JOIN pg_namespace tbl_ns ON tbl_ns.oid = tbl.relnamespace
|
|
464
|
+
JOIN pg_depend dep ON dep.refobjid = tbl.oid
|
|
465
|
+
JOIN pg_class seq ON seq.oid = dep.objid AND seq.relkind = 'S'
|
|
466
|
+
JOIN pg_namespace seq_ns ON seq_ns.oid = seq.relnamespace
|
|
467
|
+
WHERE tbl.relkind IN ('r', 'p')
|
|
468
|
+
AND dep.deptype IN ('a', 'i')
|
|
469
|
+
AND (excluded.schema_name IS NULL OR tbl_ns.nspname = excluded.schema_name)
|
|
470
|
+
`;
|
|
471
|
+
const command = `${this.getSqlCommandName()} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -At -c ${quoteShellArg(query)}`;
|
|
472
|
+
const output = String(await run(command, this.getPasswordEnvVars(password)));
|
|
473
|
+
return output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
362
474
|
}
|
|
363
|
-
async restore({
|
|
475
|
+
async restore({
|
|
476
|
+
filePath,
|
|
477
|
+
schema,
|
|
478
|
+
skipDropAllTables = false,
|
|
479
|
+
restoreMode,
|
|
480
|
+
toolchain = this.backupToolchain
|
|
481
|
+
}) {
|
|
364
482
|
const { username, host, port, database, password } = this.dbOpts;
|
|
365
483
|
let schemaOption = this.dbOpts.schema;
|
|
366
484
|
if (schema && !schemaOption) {
|
|
@@ -376,13 +494,34 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
376
494
|
const j = Math.max(1, Math.floor(cpuCores / 2));
|
|
377
495
|
const schemaNameCondition = schemaOption ? `WHERE schemaname = '${schemaOption}'` : `WHERE schemaname NOT IN ('pg_catalog', 'information_schema')`;
|
|
378
496
|
const relnamespaceCondition = schemaOption ? `WHERE relnamespace = '${schemaOption}'::regnamespace` : `WHERE tgrelid IN (SELECT oid FROM pg_class WHERE relnamespace NOT IN (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname IN ('pg_catalog', 'information_schema')))`;
|
|
379
|
-
if (
|
|
380
|
-
const
|
|
497
|
+
if (restoreMode === "preserveTables") {
|
|
498
|
+
const dropViewsCommand = `${this.getSqlCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
|
|
499
|
+
DO ${D$$} DECLARE r RECORD;
|
|
500
|
+
BEGIN
|
|
501
|
+
FOR r IN (
|
|
502
|
+
SELECT schemaname, viewname, false AS materialized FROM pg_views ${schemaNameCondition}
|
|
503
|
+
UNION ALL
|
|
504
|
+
SELECT schemaname, matviewname AS viewname, true AS materialized FROM pg_matviews ${schemaNameCondition}
|
|
505
|
+
) LOOP
|
|
506
|
+
BEGIN
|
|
507
|
+
IF r.materialized THEN
|
|
508
|
+
EXECUTE 'DROP MATERIALIZED VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.viewname) || ' CASCADE';
|
|
509
|
+
ELSE
|
|
510
|
+
EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.viewname) || ' CASCADE';
|
|
511
|
+
END IF;
|
|
512
|
+
EXCEPTION
|
|
513
|
+
WHEN OTHERS THEN
|
|
514
|
+
END;
|
|
515
|
+
END LOOP;
|
|
516
|
+
END ${D$$};"`.replace(/\n/g, " ");
|
|
517
|
+
await run(dropViewsCommand, this.getPasswordEnvVars(password, toolchain));
|
|
518
|
+
} else if (!skipDropAllTables) {
|
|
519
|
+
const dropDataCommand = `${this.getSqlCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
|
|
381
520
|
DO ${D$$} DECLARE r RECORD;
|
|
382
521
|
BEGIN
|
|
383
522
|
FOR r IN (SELECT viewname,schemaname FROM pg_views ${schemaNameCondition}) LOOP
|
|
384
523
|
BEGIN
|
|
385
|
-
EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.
|
|
524
|
+
EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.viewname) || ' CASCADE';
|
|
386
525
|
EXCEPTION
|
|
387
526
|
WHEN OTHERS THEN
|
|
388
527
|
END;
|
|
@@ -398,7 +537,7 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
398
537
|
|
|
399
538
|
FOR r IN (SELECT sequencename,schemaname FROM pg_sequences ${schemaNameCondition}) LOOP
|
|
400
539
|
BEGIN
|
|
401
|
-
EXECUTE 'DROP SEQUENCE IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.
|
|
540
|
+
EXECUTE 'DROP SEQUENCE IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.sequencename) || ' CASCADE';
|
|
402
541
|
EXCEPTION
|
|
403
542
|
WHEN OTHERS THEN
|
|
404
543
|
END;
|
|
@@ -417,21 +556,47 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
417
556
|
END LOOP;
|
|
418
557
|
|
|
419
558
|
END ${D$$};"`.replace(/\n/g, " ");
|
|
420
|
-
await run(dropDataCommand,
|
|
559
|
+
await run(dropDataCommand, this.getPasswordEnvVars(password, toolchain));
|
|
421
560
|
}
|
|
422
561
|
if (schema === schemaOption || !schemaOption) {
|
|
423
|
-
const
|
|
424
|
-
|
|
562
|
+
const restoreList = restoreMode === "preserveTables" ? await this.createRestoreListWithoutSchema(filePath, toolchain) : void 0;
|
|
563
|
+
const pgRestoreCommand = `${this.getRestoreCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} --clean --if-exists --no-owner -j ${j} ${(restoreList == null ? void 0 : restoreList.option) ?? ""} ${filePath}`;
|
|
564
|
+
try {
|
|
565
|
+
await run(pgRestoreCommand, this.getPasswordEnvVars(password, toolchain));
|
|
566
|
+
} finally {
|
|
567
|
+
if (restoreList) {
|
|
568
|
+
await fsPromises.unlink(restoreList.filePath).catch(() => {
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
425
572
|
} else {
|
|
426
573
|
const srcSchema = schema || "public";
|
|
427
|
-
const pgRestoreCommand =
|
|
428
|
-
await this
|
|
574
|
+
const pgRestoreCommand = this.buildSchemaRestoreCommand(srcSchema, schemaOption, filePath, j, toolchain);
|
|
575
|
+
await this.restoreSchema(srcSchema, schemaOption, pgRestoreCommand, toolchain);
|
|
429
576
|
}
|
|
430
577
|
}
|
|
431
|
-
async
|
|
578
|
+
async createRestoreListWithoutSchema(filePath, toolchain = this.backupToolchain) {
|
|
579
|
+
const listOutput = String(await run(`${this.getRestoreCommandName(toolchain)} --list ${quoteShellArg(filePath)}`));
|
|
580
|
+
const filteredList = listOutput.split("\n").filter((line) => !isPgRestoreSchemaTocEntry(line)).join("\n");
|
|
581
|
+
const listFilePath = import_path.default.join(
|
|
582
|
+
import_os.default.tmpdir(),
|
|
583
|
+
`nocobase-pg-restore-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.list`
|
|
584
|
+
);
|
|
585
|
+
await fsPromises.writeFile(listFilePath, filteredList);
|
|
586
|
+
return {
|
|
587
|
+
option: `-L ${quoteShellArg(listFilePath)}`,
|
|
588
|
+
filePath: listFilePath
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
buildSchemaRestoreCommand(srcSchema, _targetSchema, filePath, jobs, toolchain = this.backupToolchain) {
|
|
592
|
+
const { username, host, port, database } = this.dbOpts;
|
|
593
|
+
return `${this.getRestoreCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -n ${srcSchema} -d ${database} --clean --if-exists --no-owner -j ${jobs} ${filePath}`;
|
|
594
|
+
}
|
|
595
|
+
async restoreSchema(srcSchema, targetSchema, pgRestoreCommand, toolchain = this.backupToolchain) {
|
|
432
596
|
const { username, host, port, database, password } = this.dbOpts;
|
|
433
597
|
const ts = Date.now();
|
|
434
|
-
const
|
|
598
|
+
const sqlCommandName = this.getSqlCommandName(toolchain);
|
|
599
|
+
const preCommand = `${sqlCommandName} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
|
|
435
600
|
DO ${D$$}
|
|
436
601
|
BEGIN
|
|
437
602
|
IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = '${srcSchema}') THEN
|
|
@@ -439,7 +604,7 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
439
604
|
END IF;
|
|
440
605
|
EXECUTE 'CREATE SCHEMA ${srcSchema}';
|
|
441
606
|
END ${D$$};"`.replace(/\n/g, " ");
|
|
442
|
-
const postCommand =
|
|
607
|
+
const postCommand = `${sqlCommandName} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
|
|
443
608
|
DO ${D$$}
|
|
444
609
|
BEGIN
|
|
445
610
|
EXECUTE 'DROP SCHEMA ${targetSchema} CASCADE';
|
|
@@ -448,21 +613,21 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
448
613
|
EXECUTE 'ALTER SCHEMA ${srcSchema}_${ts} RENAME TO ${srcSchema}';
|
|
449
614
|
END IF;
|
|
450
615
|
END ${D$$};"`.replace(/\n/g, " ");
|
|
451
|
-
await run(preCommand,
|
|
616
|
+
await run(preCommand, this.getPasswordEnvVars(password, toolchain));
|
|
452
617
|
try {
|
|
453
|
-
await run(pgRestoreCommand,
|
|
618
|
+
await run(pgRestoreCommand, this.getPasswordEnvVars(password, toolchain));
|
|
454
619
|
} finally {
|
|
455
|
-
await run(postCommand,
|
|
620
|
+
await run(postCommand, this.getPasswordEnvVars(password, toolchain));
|
|
456
621
|
}
|
|
457
|
-
await this
|
|
622
|
+
await this.syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain);
|
|
458
623
|
}
|
|
459
|
-
async
|
|
624
|
+
async syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain = this.backupToolchain) {
|
|
460
625
|
const { username, host, port, database, password, tablePrefix } = this.dbOpts;
|
|
461
626
|
const collectionsTable = `${tablePrefix || ""}collections`;
|
|
462
627
|
const targetSchemaLiteral = escapeStringLiteral(targetSchema);
|
|
463
628
|
const srcSchemaLiteral = escapeStringLiteral(srcSchema);
|
|
464
629
|
const collectionsTableLiteral = escapeStringLiteral(collectionsTable);
|
|
465
|
-
const updateCollectionSchemaCommand =
|
|
630
|
+
const updateCollectionSchemaCommand = `${this.getSqlCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
|
|
466
631
|
DO ${D$$}
|
|
467
632
|
DECLARE
|
|
468
633
|
collections_table text := '${collectionsTableLiteral}';
|
|
@@ -482,7 +647,61 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
482
647
|
);
|
|
483
648
|
END IF;
|
|
484
649
|
END ${D$$};"`.replace(/\n/g, " ");
|
|
485
|
-
await run(updateCollectionSchemaCommand,
|
|
650
|
+
await run(updateCollectionSchemaCommand, this.getPasswordEnvVars(password, toolchain));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
class KingbaseAdapter extends PostgresAdapter {
|
|
654
|
+
get backupToolchain() {
|
|
655
|
+
return this.hasKingbaseToolchain() ? "kingbase" : "postgres";
|
|
656
|
+
}
|
|
657
|
+
getKingbaseBackupCommandName() {
|
|
658
|
+
return formatPathInEnv(process.env.KINGBASE_DUMP_PATH) || "sys_dump";
|
|
659
|
+
}
|
|
660
|
+
getKingbaseRestoreCommandName() {
|
|
661
|
+
return formatPathInEnv(process.env.KINGBASE_RESTORE_PATH) || "sys_restore";
|
|
662
|
+
}
|
|
663
|
+
getKingbaseSqlCommandName() {
|
|
664
|
+
return formatPathInEnv(process.env.KINGBASE_PSQL_PATH) || "ksql";
|
|
665
|
+
}
|
|
666
|
+
hasKingbaseToolchain() {
|
|
667
|
+
return this.hasCommand(this.getKingbaseBackupCommandName()) && this.hasCommand(this.getKingbaseRestoreCommandName()) && this.hasCommand(this.getKingbaseSqlCommandName());
|
|
668
|
+
}
|
|
669
|
+
getBackupCommandName(toolchain = this.backupToolchain) {
|
|
670
|
+
return toolchain === "kingbase" ? this.getKingbaseBackupCommandName() : super.getBackupCommandName(toolchain);
|
|
671
|
+
}
|
|
672
|
+
getRestoreCommandName(toolchain = this.backupToolchain) {
|
|
673
|
+
return toolchain === "kingbase" ? this.getKingbaseRestoreCommandName() : super.getRestoreCommandName(toolchain);
|
|
674
|
+
}
|
|
675
|
+
getSqlCommandName(toolchain = this.backupToolchain) {
|
|
676
|
+
return toolchain === "kingbase" ? this.getKingbaseSqlCommandName() : super.getSqlCommandName(toolchain);
|
|
677
|
+
}
|
|
678
|
+
getPasswordEnvVars(password, toolchain = this.backupToolchain) {
|
|
679
|
+
if (toolchain === "postgres") {
|
|
680
|
+
return super.getPasswordEnvVars(password, toolchain);
|
|
681
|
+
}
|
|
682
|
+
return { KINGBASE_PASSWORD: password };
|
|
683
|
+
}
|
|
684
|
+
getBackupSchema() {
|
|
685
|
+
return this.dbOpts.schema || "public";
|
|
686
|
+
}
|
|
687
|
+
buildSchemaRestoreCommand(srcSchema, targetSchema, filePath, jobs, toolchain = this.backupToolchain) {
|
|
688
|
+
if (toolchain === "postgres") {
|
|
689
|
+
return super.buildSchemaRestoreCommand(srcSchema, targetSchema, filePath, jobs, toolchain);
|
|
690
|
+
}
|
|
691
|
+
const { username, host, port, database } = this.dbOpts;
|
|
692
|
+
return `${this.getRestoreCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -g ${srcSchema} -G ${targetSchema} -d ${database} --clean --if-exists --no-owner -j ${jobs} ${filePath}`;
|
|
693
|
+
}
|
|
694
|
+
async restoreSchema(srcSchema, targetSchema, restoreCommand, toolchain = this.backupToolchain) {
|
|
695
|
+
if (toolchain === "postgres") {
|
|
696
|
+
await super.restoreSchema(srcSchema, targetSchema, restoreCommand, toolchain);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const { username, host, port, database, password } = this.dbOpts;
|
|
700
|
+
const targetSchemaIdentifier = quotePgIdentifier(targetSchema).replace(/"/g, '\\"');
|
|
701
|
+
const createSchemaCommand = `${this.getSqlCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "CREATE SCHEMA IF NOT EXISTS ${targetSchemaIdentifier};"`;
|
|
702
|
+
await run(createSchemaCommand, this.getPasswordEnvVars(password, toolchain));
|
|
703
|
+
await run(restoreCommand, this.getPasswordEnvVars(password, toolchain));
|
|
704
|
+
await this.syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain);
|
|
486
705
|
}
|
|
487
706
|
}
|
|
488
707
|
class SQLiteAdapter extends BaseDBAdapter {
|
|
@@ -503,6 +722,7 @@ class MariaDBAdapter extends MySQLAdapter {
|
|
|
503
722
|
const adapterMap = {
|
|
504
723
|
mysql: MySQLAdapter,
|
|
505
724
|
postgres: PostgresAdapter,
|
|
725
|
+
kingbase: KingbaseAdapter,
|
|
506
726
|
sqlite: SQLiteAdapter,
|
|
507
727
|
mariadb: MariaDBAdapter
|
|
508
728
|
};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
export { BackupManager } from './managers/backup';
|
|
10
10
|
export type { BackupSettings, BackupCreator, BackupTaskResult } from './managers/backup';
|
|
11
11
|
export { RestoreManager, RestoreOptions } from './managers/restore';
|
|
12
|
-
export { BACKUP_EXTENSION } from './utils';
|
|
12
|
+
export { BACKUP_EXTENSION, METADATA_EXTENSION, Extractor, compressDirectoryToBackupArchive } from './utils';
|
|
13
13
|
export { default } from './plugin';
|
package/dist/server/index.js
CHANGED
|
@@ -38,8 +38,11 @@ var server_exports = {};
|
|
|
38
38
|
__export(server_exports, {
|
|
39
39
|
BACKUP_EXTENSION: () => import_utils.BACKUP_EXTENSION,
|
|
40
40
|
BackupManager: () => import_backup.BackupManager,
|
|
41
|
+
Extractor: () => import_utils.Extractor,
|
|
42
|
+
METADATA_EXTENSION: () => import_utils.METADATA_EXTENSION,
|
|
41
43
|
RestoreManager: () => import_restore.RestoreManager,
|
|
42
44
|
RestoreOptions: () => import_restore.RestoreOptions,
|
|
45
|
+
compressDirectoryToBackupArchive: () => import_utils.compressDirectoryToBackupArchive,
|
|
43
46
|
default: () => import_plugin.default
|
|
44
47
|
});
|
|
45
48
|
module.exports = __toCommonJS(server_exports);
|
|
@@ -51,6 +54,9 @@ var import_plugin = __toESM(require("./plugin"));
|
|
|
51
54
|
0 && (module.exports = {
|
|
52
55
|
BACKUP_EXTENSION,
|
|
53
56
|
BackupManager,
|
|
57
|
+
Extractor,
|
|
58
|
+
METADATA_EXTENSION,
|
|
54
59
|
RestoreManager,
|
|
55
|
-
RestoreOptions
|
|
60
|
+
RestoreOptions,
|
|
61
|
+
compressDirectoryToBackupArchive
|
|
56
62
|
});
|
|
@@ -17,10 +17,14 @@ export interface BackupSettings {
|
|
|
17
17
|
keep?: number;
|
|
18
18
|
scheduled: boolean;
|
|
19
19
|
cron: string;
|
|
20
|
+
/**
|
|
21
|
+
* @deprecated Prefer excludeTables. includeTables may miss dependent database objects.
|
|
22
|
+
*/
|
|
20
23
|
includeTables?: string[];
|
|
21
24
|
excludeTables?: string[];
|
|
22
25
|
description?: string;
|
|
23
26
|
createdBy?: BackupCreator;
|
|
27
|
+
metadata?: Record<string, unknown>;
|
|
24
28
|
}
|
|
25
29
|
export interface BackupFile {
|
|
26
30
|
name: string;
|
|
@@ -50,7 +50,7 @@ var import_utils = require("@nocobase/utils");
|
|
|
50
50
|
var import_util = require("util");
|
|
51
51
|
var import_database = require("../adapters/database");
|
|
52
52
|
var import_utils2 = require("../utils");
|
|
53
|
-
const BACKUP_METADATA_VERSION =
|
|
53
|
+
const BACKUP_METADATA_VERSION = 2;
|
|
54
54
|
class BackupManager {
|
|
55
55
|
app;
|
|
56
56
|
ctx;
|
|
@@ -99,7 +99,10 @@ class BackupManager {
|
|
|
99
99
|
}
|
|
100
100
|
async backup(fileBaseName, opts) {
|
|
101
101
|
const contentPath = import_path.default.join(this.#tempDir, fileBaseName);
|
|
102
|
-
|
|
102
|
+
const runtimeTables = [...this.app.db.collections.values()].filter((collection) => collection.dataCategory === "runtime").map((collection) => collection.getTableNameWithSchemaAsString());
|
|
103
|
+
const backupOptions = { ...this.#settings, ...opts ?? {} };
|
|
104
|
+
backupOptions.excludeTables = [.../* @__PURE__ */ new Set([...backupOptions.excludeTables ?? [], ...runtimeTables])];
|
|
105
|
+
return this.#runBackupTask(backupOptions, fileBaseName, contentPath);
|
|
103
106
|
}
|
|
104
107
|
async destroy(fileName) {
|
|
105
108
|
const filePath = this.#getValidatedFilePath(fileName);
|
|
@@ -187,6 +190,7 @@ class BackupManager {
|
|
|
187
190
|
};
|
|
188
191
|
});
|
|
189
192
|
const metadata = {
|
|
193
|
+
...opts.metadata ?? {},
|
|
190
194
|
metadataVersion: BACKUP_METADATA_VERSION,
|
|
191
195
|
enableFilesBackup: opts.enableFilesBackup,
|
|
192
196
|
version: await this.app.version.get(),
|
|
@@ -194,6 +198,7 @@ class BackupManager {
|
|
|
194
198
|
createdBy: opts.createdBy,
|
|
195
199
|
database: {
|
|
196
200
|
dialect,
|
|
201
|
+
toolchain: this.#dbAdapter.backupToolchain,
|
|
197
202
|
underscored,
|
|
198
203
|
tablePrefix,
|
|
199
204
|
schema,
|
|
@@ -142,7 +142,9 @@ class RestoreManager {
|
|
|
142
142
|
await this.#dbAdapter.restore({
|
|
143
143
|
filePath: import_path.default.join(extractedDir, dbFile),
|
|
144
144
|
schema: metadata.database.schema,
|
|
145
|
-
skipDropAllTables: (options == null ? void 0 : options.skipDropAllTables) === true
|
|
145
|
+
skipDropAllTables: (options == null ? void 0 : options.skipDropAllTables) === true,
|
|
146
|
+
restoreMode: options == null ? void 0 : options.restoreMode,
|
|
147
|
+
toolchain: this.#resolveRestoreToolchain(metadata)
|
|
146
148
|
});
|
|
147
149
|
this.ctx.logger.info("Database restored successfully", { module: import_utils2.BACKUPS });
|
|
148
150
|
await this.#restoreFilesAndCleanup(restoreUploads, extractedDir);
|
|
@@ -195,7 +197,7 @@ class RestoreManager {
|
|
|
195
197
|
await this.#restoreFilesAndCleanup(uploadsExist, extractedDir);
|
|
196
198
|
await this.ctx.app.runCommand("upgrade");
|
|
197
199
|
} else {
|
|
198
|
-
if (!tolerentMode && this.#dbAdapter.dbOpts.dialect
|
|
200
|
+
if (!tolerentMode && this.#isPostgresLikeDialect(this.#dbAdapter.dbOpts.dialect)) {
|
|
199
201
|
const backupClientVersion = Number((0, import_utils2.toMajorVersion)(metadata.database.backupClientVersion));
|
|
200
202
|
const dbServerVersion = Number((0, import_utils2.toMajorVersion)(dbVersion));
|
|
201
203
|
if (backupClientVersion > 16 && dbServerVersion <= 16) {
|
|
@@ -248,7 +250,7 @@ class RestoreManager {
|
|
|
248
250
|
if ((metadata.database.tablePrefix || "") !== (tablePrefix || "")) {
|
|
249
251
|
throw new Error(this.#t("Database table prefix mismatch"));
|
|
250
252
|
}
|
|
251
|
-
const forceSchemaRestore = (options == null ? void 0 : options.forceSchemaRestore) === true && dialect
|
|
253
|
+
const forceSchemaRestore = (options == null ? void 0 : options.forceSchemaRestore) === true && this.#isPostgresLikeDialect(dialect);
|
|
252
254
|
if (!forceSchemaRestore) {
|
|
253
255
|
if (((_b = (_a = this.ctx.request) == null ? void 0 : _a.body) == null ? void 0 : _b.dbSchema) && ((_d = (_c = this.ctx.request) == null ? void 0 : _c.body) == null ? void 0 : _d.dbSchema) !== (schema || "public")) {
|
|
254
256
|
throw new Error(this.#t("Database schema mismatch"));
|
|
@@ -289,6 +291,25 @@ class RestoreManager {
|
|
|
289
291
|
}
|
|
290
292
|
return true;
|
|
291
293
|
}
|
|
294
|
+
#isPostgresLikeDialect(dialect) {
|
|
295
|
+
return dialect === "postgres" || dialect === "kingbase";
|
|
296
|
+
}
|
|
297
|
+
#resolveRestoreToolchain(metadata) {
|
|
298
|
+
if (metadata.database.dialect !== "kingbase") {
|
|
299
|
+
return void 0;
|
|
300
|
+
}
|
|
301
|
+
if (metadata.database.toolchain === "kingbase" || metadata.database.toolchain === "postgres") {
|
|
302
|
+
return metadata.database.toolchain;
|
|
303
|
+
}
|
|
304
|
+
const backupClientVersion = metadata.database.backupClientVersion || "";
|
|
305
|
+
if (/sys_dump/i.test(backupClientVersion)) {
|
|
306
|
+
return "kingbase";
|
|
307
|
+
}
|
|
308
|
+
if (/pg_dump|postgresql/i.test(backupClientVersion)) {
|
|
309
|
+
return "postgres";
|
|
310
|
+
}
|
|
311
|
+
return this.#dbAdapter.backupToolchain;
|
|
312
|
+
}
|
|
292
313
|
async #decompressFiles(filePath, password) {
|
|
293
314
|
const fileBaseName = import_path.default.basename(filePath, `.${import_utils2.BACKUP_EXTENSION}`);
|
|
294
315
|
const outputDir = import_path.default.join(this.#tempDir, fileBaseName);
|
|
@@ -372,7 +393,9 @@ class RestoreManager {
|
|
|
372
393
|
await this.#dbAdapter.restore({
|
|
373
394
|
filePath: import_path.default.join(extractedDir, dbFile),
|
|
374
395
|
schema: metadata.database.schema,
|
|
375
|
-
skipDropAllTables: (options == null ? void 0 : options.skipDropAllTables) === true
|
|
396
|
+
skipDropAllTables: (options == null ? void 0 : options.skipDropAllTables) === true,
|
|
397
|
+
restoreMode: options == null ? void 0 : options.restoreMode,
|
|
398
|
+
toolchain: this.#resolveRestoreToolchain(metadata)
|
|
376
399
|
});
|
|
377
400
|
this.ctx.logger.info("Database restored successfully", { module: import_utils2.BACKUPS });
|
|
378
401
|
if (restoreUploads) {
|
package/dist/server/utils.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export declare class Extractor extends Writable {
|
|
|
33
33
|
private extract;
|
|
34
34
|
promise(): Promise<void>;
|
|
35
35
|
}
|
|
36
|
+
export declare function compressDirectoryToBackupArchive(sourceDir: string, backupFilePath: string): Promise<void>;
|
|
36
37
|
export declare function humanFileSize(bytes: number, si?: boolean, dp?: number): string;
|
|
37
38
|
export declare function getDBVersion(db: Database): Promise<string>;
|
|
38
39
|
export declare function isQsTruly(value?: string | boolean): any;
|