@nocobase/plugin-backups 2.2.0-alpha.1 → 2.2.0-alpha.2
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/{105.064f69093e98a2dd.js → 105.d20cb8a299876029.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/locale/de-DE.json +1 -1
- package/dist/locale/en-US.json +1 -1
- package/dist/locale/es-ES.json +1 -1
- package/dist/locale/fr-FR.json +1 -1
- package/dist/locale/hu-HU.json +1 -1
- package/dist/locale/id-ID.json +1 -1
- package/dist/locale/it-IT.json +1 -1
- package/dist/locale/ja-JP.json +1 -1
- package/dist/locale/ko-KR.json +1 -1
- package/dist/locale/nl-NL.json +1 -1
- package/dist/locale/pt-BR.json +1 -1
- package/dist/locale/ru-RU.json +1 -1
- package/dist/locale/tr-TR.json +1 -1
- package/dist/locale/uk-UA.json +1 -1
- package/dist/locale/vi-VN.json +1 -1
- package/dist/locale/zh-CN.json +1 -1
- package/dist/locale/zh-TW.json +1 -1
- 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 +265 -34
- 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
|
@@ -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(".")) {
|
|
@@ -75,6 +96,11 @@ const qualifyPgTablePattern = (table, schema) => {
|
|
|
75
96
|
}
|
|
76
97
|
return `${schema}.${tablePattern}`;
|
|
77
98
|
};
|
|
99
|
+
const assertPostgresSchemaIdentifier = (schema) => {
|
|
100
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,62}$/.test(schema)) {
|
|
101
|
+
throw new Error(`Invalid PostgreSQL schema: ${schema}`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
78
104
|
class BaseDBAdapter {
|
|
79
105
|
constructor(dbOpts) {
|
|
80
106
|
this.dbOpts = dbOpts;
|
|
@@ -88,10 +114,18 @@ class BaseDBAdapter {
|
|
|
88
114
|
(0, import_child_process.execSync)(`${command} --version`);
|
|
89
115
|
} catch (error) {
|
|
90
116
|
throw new Error(
|
|
91
|
-
`Command ${command} not found, please install it first. Check reference here: https://docs.nocobase.com/
|
|
117
|
+
`Command ${command} not found, please install it first. Check reference here: https://docs.nocobase.com/ops-management/backup-manager/`
|
|
92
118
|
);
|
|
93
119
|
}
|
|
94
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
|
+
}
|
|
95
129
|
}
|
|
96
130
|
class MySQLAdapter extends BaseDBAdapter {
|
|
97
131
|
#backupCmd = "mysqldump";
|
|
@@ -140,6 +174,7 @@ class MySQLAdapter extends BaseDBAdapter {
|
|
|
140
174
|
"--set-gtid-purged=OFF",
|
|
141
175
|
"--routines",
|
|
142
176
|
"--triggers",
|
|
177
|
+
"--events",
|
|
143
178
|
...version && version > 7 ? ["--column-statistics=0"] : [],
|
|
144
179
|
database
|
|
145
180
|
];
|
|
@@ -232,9 +267,47 @@ class MySQLAdapter extends BaseDBAdapter {
|
|
|
232
267
|
return "";
|
|
233
268
|
}
|
|
234
269
|
}
|
|
235
|
-
async restore({ filePath, skipDropAllTables = false }) {
|
|
270
|
+
async restore({ filePath, skipDropAllTables = false, restoreMode }) {
|
|
236
271
|
const { username, host, port, database, password } = this.dbOpts;
|
|
237
|
-
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) {
|
|
238
311
|
const dropDataCommand = `mysql -u ${username} -h ${host} ${port ? `-P ${port}` : ""} --protocol=tcp -D ${database} -e "
|
|
239
312
|
DELIMITER $$
|
|
240
313
|
DROP PROCEDURE IF EXISTS drop_all_tables_and_triggers$$
|
|
@@ -322,24 +395,36 @@ class MySQLAdapter extends BaseDBAdapter {
|
|
|
322
395
|
}
|
|
323
396
|
}
|
|
324
397
|
class PostgresAdapter extends BaseDBAdapter {
|
|
325
|
-
get
|
|
398
|
+
get backupToolchain() {
|
|
399
|
+
return "postgres";
|
|
400
|
+
}
|
|
401
|
+
getBackupCommandName(_toolchain = this.backupToolchain) {
|
|
326
402
|
return formatPathInEnv(process.env.PG_DUMP_PATH) || "pg_dump";
|
|
327
403
|
}
|
|
328
|
-
|
|
404
|
+
getRestoreCommandName(_toolchain = this.backupToolchain) {
|
|
329
405
|
return formatPathInEnv(process.env.PG_RESTORE_PATH) || "pg_restore";
|
|
330
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
|
+
}
|
|
331
416
|
async check(op) {
|
|
332
417
|
switch (op) {
|
|
333
418
|
case "backup":
|
|
334
|
-
this.assertCommand(this
|
|
419
|
+
this.assertCommand(this.getBackupCommandName());
|
|
335
420
|
break;
|
|
336
421
|
case "restore":
|
|
337
|
-
this.assertCommand(this
|
|
422
|
+
this.assertCommand(this.getRestoreCommandName());
|
|
338
423
|
break;
|
|
339
424
|
}
|
|
340
425
|
}
|
|
341
426
|
async clientVersion(op) {
|
|
342
|
-
const cmd = op === "backup" ? this
|
|
427
|
+
const cmd = op === "backup" ? this.getBackupCommandName() : this.getRestoreCommandName();
|
|
343
428
|
try {
|
|
344
429
|
return (0, import_child_process.execSync)(`${cmd} --version`).toString();
|
|
345
430
|
} catch (_error) {
|
|
@@ -347,31 +432,96 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
347
432
|
}
|
|
348
433
|
}
|
|
349
434
|
async backup({ dir, includeTables, excludeTables }) {
|
|
350
|
-
const { username, host, port, database, password
|
|
435
|
+
const { username, host, port, database, password } = this.dbOpts;
|
|
351
436
|
const filePath = `${dir}/data`;
|
|
437
|
+
const backupSchema = this.getBackupSchema();
|
|
352
438
|
const schemaOption = backupSchema ? `--schema=${backupSchema}` : "";
|
|
439
|
+
const expandedExcludeTables = Array.isArray(excludeTables) ? [.../* @__PURE__ */ new Set([...excludeTables, ...await this.getOwnedSequenceTables(excludeTables, backupSchema)])] : [];
|
|
353
440
|
const includeOption = Array.isArray(includeTables) && includeTables.length ? includeTables.map((table) => `-t ${quotePgTablePattern(qualifyPgTablePattern(table, backupSchema))}`).join(" ") : "";
|
|
354
|
-
const excludeOption =
|
|
355
|
-
const command = `${this
|
|
356
|
-
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));
|
|
357
444
|
}
|
|
358
|
-
async
|
|
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);
|
|
474
|
+
}
|
|
475
|
+
async restore({
|
|
476
|
+
filePath,
|
|
477
|
+
schema,
|
|
478
|
+
skipDropAllTables = false,
|
|
479
|
+
restoreMode,
|
|
480
|
+
toolchain = this.backupToolchain
|
|
481
|
+
}) {
|
|
359
482
|
const { username, host, port, database, password } = this.dbOpts;
|
|
360
483
|
let schemaOption = this.dbOpts.schema;
|
|
361
484
|
if (schema && !schemaOption) {
|
|
362
485
|
schemaOption = "public";
|
|
363
486
|
}
|
|
487
|
+
if (schema) {
|
|
488
|
+
assertPostgresSchemaIdentifier(schema);
|
|
489
|
+
}
|
|
490
|
+
if (schemaOption) {
|
|
491
|
+
assertPostgresSchemaIdentifier(schemaOption);
|
|
492
|
+
}
|
|
364
493
|
const cpuCores = import_os.default.cpus().length;
|
|
365
494
|
const j = Math.max(1, Math.floor(cpuCores / 2));
|
|
366
495
|
const schemaNameCondition = schemaOption ? `WHERE schemaname = '${schemaOption}'` : `WHERE schemaname NOT IN ('pg_catalog', 'information_schema')`;
|
|
367
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')))`;
|
|
368
|
-
if (
|
|
369
|
-
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 "
|
|
370
520
|
DO ${D$$} DECLARE r RECORD;
|
|
371
521
|
BEGIN
|
|
372
522
|
FOR r IN (SELECT viewname,schemaname FROM pg_views ${schemaNameCondition}) LOOP
|
|
373
523
|
BEGIN
|
|
374
|
-
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';
|
|
375
525
|
EXCEPTION
|
|
376
526
|
WHEN OTHERS THEN
|
|
377
527
|
END;
|
|
@@ -387,7 +537,7 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
387
537
|
|
|
388
538
|
FOR r IN (SELECT sequencename,schemaname FROM pg_sequences ${schemaNameCondition}) LOOP
|
|
389
539
|
BEGIN
|
|
390
|
-
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';
|
|
391
541
|
EXCEPTION
|
|
392
542
|
WHEN OTHERS THEN
|
|
393
543
|
END;
|
|
@@ -406,21 +556,47 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
406
556
|
END LOOP;
|
|
407
557
|
|
|
408
558
|
END ${D$$};"`.replace(/\n/g, " ");
|
|
409
|
-
await run(dropDataCommand,
|
|
559
|
+
await run(dropDataCommand, this.getPasswordEnvVars(password, toolchain));
|
|
410
560
|
}
|
|
411
561
|
if (schema === schemaOption || !schemaOption) {
|
|
412
|
-
const
|
|
413
|
-
|
|
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
|
+
}
|
|
414
572
|
} else {
|
|
415
573
|
const srcSchema = schema || "public";
|
|
416
|
-
const pgRestoreCommand =
|
|
417
|
-
await this
|
|
574
|
+
const pgRestoreCommand = this.buildSchemaRestoreCommand(srcSchema, schemaOption, filePath, j, toolchain);
|
|
575
|
+
await this.restoreSchema(srcSchema, schemaOption, pgRestoreCommand, toolchain);
|
|
418
576
|
}
|
|
419
577
|
}
|
|
420
|
-
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) {
|
|
421
596
|
const { username, host, port, database, password } = this.dbOpts;
|
|
422
597
|
const ts = Date.now();
|
|
423
|
-
const
|
|
598
|
+
const sqlCommandName = this.getSqlCommandName(toolchain);
|
|
599
|
+
const preCommand = `${sqlCommandName} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
|
|
424
600
|
DO ${D$$}
|
|
425
601
|
BEGIN
|
|
426
602
|
IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = '${srcSchema}') THEN
|
|
@@ -428,7 +604,7 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
428
604
|
END IF;
|
|
429
605
|
EXECUTE 'CREATE SCHEMA ${srcSchema}';
|
|
430
606
|
END ${D$$};"`.replace(/\n/g, " ");
|
|
431
|
-
const postCommand =
|
|
607
|
+
const postCommand = `${sqlCommandName} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
|
|
432
608
|
DO ${D$$}
|
|
433
609
|
BEGIN
|
|
434
610
|
EXECUTE 'DROP SCHEMA ${targetSchema} CASCADE';
|
|
@@ -437,21 +613,21 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
437
613
|
EXECUTE 'ALTER SCHEMA ${srcSchema}_${ts} RENAME TO ${srcSchema}';
|
|
438
614
|
END IF;
|
|
439
615
|
END ${D$$};"`.replace(/\n/g, " ");
|
|
440
|
-
await run(preCommand,
|
|
616
|
+
await run(preCommand, this.getPasswordEnvVars(password, toolchain));
|
|
441
617
|
try {
|
|
442
|
-
await run(pgRestoreCommand,
|
|
618
|
+
await run(pgRestoreCommand, this.getPasswordEnvVars(password, toolchain));
|
|
443
619
|
} finally {
|
|
444
|
-
await run(postCommand,
|
|
620
|
+
await run(postCommand, this.getPasswordEnvVars(password, toolchain));
|
|
445
621
|
}
|
|
446
|
-
await this
|
|
622
|
+
await this.syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain);
|
|
447
623
|
}
|
|
448
|
-
async
|
|
624
|
+
async syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain = this.backupToolchain) {
|
|
449
625
|
const { username, host, port, database, password, tablePrefix } = this.dbOpts;
|
|
450
626
|
const collectionsTable = `${tablePrefix || ""}collections`;
|
|
451
627
|
const targetSchemaLiteral = escapeStringLiteral(targetSchema);
|
|
452
628
|
const srcSchemaLiteral = escapeStringLiteral(srcSchema);
|
|
453
629
|
const collectionsTableLiteral = escapeStringLiteral(collectionsTable);
|
|
454
|
-
const updateCollectionSchemaCommand =
|
|
630
|
+
const updateCollectionSchemaCommand = `${this.getSqlCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
|
|
455
631
|
DO ${D$$}
|
|
456
632
|
DECLARE
|
|
457
633
|
collections_table text := '${collectionsTableLiteral}';
|
|
@@ -471,7 +647,61 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
471
647
|
);
|
|
472
648
|
END IF;
|
|
473
649
|
END ${D$$};"`.replace(/\n/g, " ");
|
|
474
|
-
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);
|
|
475
705
|
}
|
|
476
706
|
}
|
|
477
707
|
class SQLiteAdapter extends BaseDBAdapter {
|
|
@@ -492,6 +722,7 @@ class MariaDBAdapter extends MySQLAdapter {
|
|
|
492
722
|
const adapterMap = {
|
|
493
723
|
mysql: MySQLAdapter,
|
|
494
724
|
postgres: PostgresAdapter,
|
|
725
|
+
kingbase: KingbaseAdapter,
|
|
495
726
|
sqlite: SQLiteAdapter,
|
|
496
727
|
mariadb: MariaDBAdapter
|
|
497
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;
|