@nocobase/plugin-backups 2.2.0-alpha.1 → 2.2.0-alpha.10
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.72267b98c0ce9379.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 +3 -2
- 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 +3 -2
- 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 +368 -56
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +7 -1
- package/dist/server/managers/backup.d.ts +10 -2
- package/dist/server/managers/backup.js +14 -3
- package/dist/server/managers/restore.d.ts +1 -0
- package/dist/server/managers/restore.js +27 -4
- package/dist/server/resourcers/backup-cli.d.ts +9 -9
- package/dist/server/resourcers/backups.d.ts +10 -10
- 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
|
@@ -48,26 +48,75 @@ var import_promises = require("stream/promises");
|
|
|
48
48
|
var import_util = require("util");
|
|
49
49
|
var import_utils = require("../utils");
|
|
50
50
|
const exec = (0, import_util.promisify)(import_child_process.exec);
|
|
51
|
-
const D$$ = import_os.default.platform() === "win32" ? "$$" : "\\$\\$";
|
|
52
51
|
const STREAM_BUFFER_SIZE = 2 * 1024 * 1024;
|
|
53
52
|
const run = async (command, envVars = {}) => {
|
|
54
53
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
54
|
+
const result = await exec(command, { env: { ...process.env, ...envVars } });
|
|
55
|
+
if (typeof result === "string") {
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
if (result && typeof result === "object" && "stdout" in result) {
|
|
59
|
+
const { stdout } = result;
|
|
60
|
+
return typeof stdout === "string" ? stdout : String(stdout ?? "");
|
|
61
|
+
}
|
|
62
|
+
return "";
|
|
57
63
|
} catch (error) {
|
|
58
64
|
throw new Error(`${error.message}`);
|
|
59
65
|
}
|
|
60
66
|
};
|
|
67
|
+
const runSpawn = async (command, args, envVars = {}) => {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const child = (0, import_child_process.spawn)(command, args, {
|
|
70
|
+
env: { ...process.env, ...envVars },
|
|
71
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
72
|
+
});
|
|
73
|
+
const stdout = [];
|
|
74
|
+
const stderr = [];
|
|
75
|
+
let settled = false;
|
|
76
|
+
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
77
|
+
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
78
|
+
child.on("error", (error) => {
|
|
79
|
+
settled = true;
|
|
80
|
+
reject(error);
|
|
81
|
+
});
|
|
82
|
+
child.on("close", (code) => {
|
|
83
|
+
if (settled) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (code === 0) {
|
|
87
|
+
settled = true;
|
|
88
|
+
resolve(Buffer.concat(stdout).toString());
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const errorMessage = Buffer.concat(stderr).toString().trim() || `${command} exited with code ${code}`;
|
|
92
|
+
settled = true;
|
|
93
|
+
reject(new Error(errorMessage));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
};
|
|
61
97
|
const formatPathInEnv = (path2) => {
|
|
62
|
-
if (path2
|
|
63
|
-
return
|
|
98
|
+
if ((path2 == null ? void 0 : path2.startsWith('"')) && path2.endsWith('"') || (path2 == null ? void 0 : path2.startsWith("'")) && path2.endsWith("'")) {
|
|
99
|
+
return path2.slice(1, -1);
|
|
64
100
|
}
|
|
65
101
|
return path2;
|
|
66
102
|
};
|
|
67
103
|
const escapeStringLiteral = (value) => String(value).replace(/'/g, "''");
|
|
68
104
|
const quotePgIdentifier = (value) => `"${String(value).replace(/"/g, '""')}"`;
|
|
69
|
-
const
|
|
70
|
-
const
|
|
105
|
+
const quotePgTablePattern = (table) => String(table).split(".").map(quotePgIdentifier).join(".");
|
|
106
|
+
const isPgRestoreSchemaTocEntry = (line) => /^\d+;\s+\d+\s+\d+\s+SCHEMA\s+-\s+/.test(line);
|
|
107
|
+
const parsePgTableReference = (table, defaultSchema) => {
|
|
108
|
+
const parts = String(table).split(".");
|
|
109
|
+
if (parts.length > 1) {
|
|
110
|
+
return {
|
|
111
|
+
schema: parts[0],
|
|
112
|
+
table: parts.slice(1).join(".")
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
schema: defaultSchema,
|
|
117
|
+
table: parts[0]
|
|
118
|
+
};
|
|
119
|
+
};
|
|
71
120
|
const qualifyPgTablePattern = (table, schema) => {
|
|
72
121
|
const tablePattern = String(table);
|
|
73
122
|
if (!schema || tablePattern.includes(".")) {
|
|
@@ -75,6 +124,11 @@ const qualifyPgTablePattern = (table, schema) => {
|
|
|
75
124
|
}
|
|
76
125
|
return `${schema}.${tablePattern}`;
|
|
77
126
|
};
|
|
127
|
+
const assertPostgresSchemaIdentifier = (schema) => {
|
|
128
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,62}$/.test(schema)) {
|
|
129
|
+
throw new Error(`Invalid PostgreSQL schema: ${schema}`);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
78
132
|
class BaseDBAdapter {
|
|
79
133
|
constructor(dbOpts) {
|
|
80
134
|
this.dbOpts = dbOpts;
|
|
@@ -84,14 +138,17 @@ class BaseDBAdapter {
|
|
|
84
138
|
async clientVersion(_) {
|
|
85
139
|
}
|
|
86
140
|
assertCommand = (command) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} catch (error) {
|
|
141
|
+
const result = (0, import_child_process.spawnSync)(command, ["--version"], { encoding: "utf8" });
|
|
142
|
+
if (result.error || result.status !== 0) {
|
|
90
143
|
throw new Error(
|
|
91
|
-
`Command ${command} not found, please install it first. Check reference here: https://docs.nocobase.com/
|
|
144
|
+
`Command ${command} not found, please install it first. Check reference here: https://docs.nocobase.com/ops-management/backup-manager/`
|
|
92
145
|
);
|
|
93
146
|
}
|
|
94
147
|
};
|
|
148
|
+
hasCommand(command) {
|
|
149
|
+
const result = (0, import_child_process.spawnSync)(command, ["--version"], { encoding: "utf8" });
|
|
150
|
+
return !result.error && result.status === 0;
|
|
151
|
+
}
|
|
95
152
|
}
|
|
96
153
|
class MySQLAdapter extends BaseDBAdapter {
|
|
97
154
|
#backupCmd = "mysqldump";
|
|
@@ -140,6 +197,7 @@ class MySQLAdapter extends BaseDBAdapter {
|
|
|
140
197
|
"--set-gtid-purged=OFF",
|
|
141
198
|
"--routines",
|
|
142
199
|
"--triggers",
|
|
200
|
+
"--events",
|
|
143
201
|
...version && version > 7 ? ["--column-statistics=0"] : [],
|
|
144
202
|
database
|
|
145
203
|
];
|
|
@@ -232,9 +290,47 @@ class MySQLAdapter extends BaseDBAdapter {
|
|
|
232
290
|
return "";
|
|
233
291
|
}
|
|
234
292
|
}
|
|
235
|
-
async restore({ filePath, skipDropAllTables = false }) {
|
|
293
|
+
async restore({ filePath, skipDropAllTables = false, restoreMode }) {
|
|
236
294
|
const { username, host, port, database, password } = this.dbOpts;
|
|
237
|
-
if (
|
|
295
|
+
if (restoreMode === "preserveTables") {
|
|
296
|
+
const dropViewsCommand = `mysql -u ${username} -h ${host} ${port ? `-P ${port}` : ""} --protocol=tcp -D ${database} -e "
|
|
297
|
+
DELIMITER $$
|
|
298
|
+
DROP PROCEDURE IF EXISTS drop_all_views$$
|
|
299
|
+
CREATE PROCEDURE drop_all_views()
|
|
300
|
+
BEGIN
|
|
301
|
+
DECLARE _done INT DEFAULT FALSE;
|
|
302
|
+
DECLARE _viewName VARCHAR(255);
|
|
303
|
+
|
|
304
|
+
DECLARE _cursor CURSOR FOR
|
|
305
|
+
SELECT table_name
|
|
306
|
+
FROM information_schema.VIEWS
|
|
307
|
+
WHERE table_schema = SCHEMA();
|
|
308
|
+
|
|
309
|
+
DECLARE CONTINUE HANDLER FOR NOT FOUND SET _done = TRUE;
|
|
310
|
+
|
|
311
|
+
OPEN _cursor;
|
|
312
|
+
|
|
313
|
+
REPEAT
|
|
314
|
+
FETCH _cursor INTO _viewName;
|
|
315
|
+
|
|
316
|
+
IF NOT _done THEN
|
|
317
|
+
SET @stmt_sql = CONCAT('DROP VIEW IF EXISTS ', _viewName);
|
|
318
|
+
PREPARE stmt FROM @stmt_sql;
|
|
319
|
+
EXECUTE stmt;
|
|
320
|
+
DEALLOCATE PREPARE stmt;
|
|
321
|
+
END IF;
|
|
322
|
+
|
|
323
|
+
UNTIL _done END REPEAT;
|
|
324
|
+
|
|
325
|
+
CLOSE _cursor;
|
|
326
|
+
END$$
|
|
327
|
+
|
|
328
|
+
CALL drop_all_views()$$
|
|
329
|
+
DROP PROCEDURE drop_all_views$$
|
|
330
|
+
DELIMITER ;
|
|
331
|
+
"`;
|
|
332
|
+
await run(dropViewsCommand, { MYSQL_PWD: password });
|
|
333
|
+
} else if (!skipDropAllTables) {
|
|
238
334
|
const dropDataCommand = `mysql -u ${username} -h ${host} ${port ? `-P ${port}` : ""} --protocol=tcp -D ${database} -e "
|
|
239
335
|
DELIMITER $$
|
|
240
336
|
DROP PROCEDURE IF EXISTS drop_all_tables_and_triggers$$
|
|
@@ -322,56 +418,167 @@ class MySQLAdapter extends BaseDBAdapter {
|
|
|
322
418
|
}
|
|
323
419
|
}
|
|
324
420
|
class PostgresAdapter extends BaseDBAdapter {
|
|
325
|
-
get
|
|
421
|
+
get backupToolchain() {
|
|
422
|
+
return "postgres";
|
|
423
|
+
}
|
|
424
|
+
getBackupCommandName(_toolchain = this.backupToolchain) {
|
|
326
425
|
return formatPathInEnv(process.env.PG_DUMP_PATH) || "pg_dump";
|
|
327
426
|
}
|
|
328
|
-
|
|
427
|
+
getRestoreCommandName(_toolchain = this.backupToolchain) {
|
|
329
428
|
return formatPathInEnv(process.env.PG_RESTORE_PATH) || "pg_restore";
|
|
330
429
|
}
|
|
430
|
+
getSqlCommandName(_toolchain = this.backupToolchain) {
|
|
431
|
+
return formatPathInEnv(process.env.PSQL_PATH) || "psql";
|
|
432
|
+
}
|
|
433
|
+
getPasswordEnvVars(password, _toolchain = this.backupToolchain) {
|
|
434
|
+
return { PGPASSWORD: password };
|
|
435
|
+
}
|
|
436
|
+
getBackupSchema() {
|
|
437
|
+
return this.dbOpts.schema;
|
|
438
|
+
}
|
|
331
439
|
async check(op) {
|
|
332
440
|
switch (op) {
|
|
333
441
|
case "backup":
|
|
334
|
-
this.assertCommand(this
|
|
442
|
+
this.assertCommand(this.getBackupCommandName());
|
|
335
443
|
break;
|
|
336
444
|
case "restore":
|
|
337
|
-
this.assertCommand(this
|
|
445
|
+
this.assertCommand(this.getRestoreCommandName());
|
|
338
446
|
break;
|
|
339
447
|
}
|
|
340
448
|
}
|
|
341
449
|
async clientVersion(op) {
|
|
342
|
-
const cmd = op === "backup" ? this
|
|
450
|
+
const cmd = op === "backup" ? this.getBackupCommandName() : this.getRestoreCommandName();
|
|
343
451
|
try {
|
|
344
|
-
return
|
|
452
|
+
return await runSpawn(cmd, ["--version"]);
|
|
345
453
|
} catch (_error) {
|
|
346
454
|
return void 0;
|
|
347
455
|
}
|
|
348
456
|
}
|
|
349
457
|
async backup({ dir, includeTables, excludeTables }) {
|
|
350
|
-
const {
|
|
458
|
+
const { database, password } = this.dbOpts;
|
|
351
459
|
const filePath = `${dir}/data`;
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
|
|
460
|
+
const backupSchema = this.getBackupSchema();
|
|
461
|
+
const expandedExcludeTables = Array.isArray(excludeTables) ? [.../* @__PURE__ */ new Set([...excludeTables, ...await this.getOwnedSequenceTables(excludeTables, backupSchema)])] : [];
|
|
462
|
+
const includeArgs = Array.isArray(includeTables) && includeTables.length ? includeTables.flatMap((table) => ["-t", quotePgTablePattern(qualifyPgTablePattern(table, backupSchema))]) : [];
|
|
463
|
+
const excludeArgs = expandedExcludeTables.length ? expandedExcludeTables.flatMap((table) => [
|
|
464
|
+
"-T",
|
|
465
|
+
quotePgTablePattern(qualifyPgTablePattern(table, backupSchema))
|
|
466
|
+
]) : [];
|
|
467
|
+
const args = [
|
|
468
|
+
...includeArgs,
|
|
469
|
+
...excludeArgs,
|
|
470
|
+
...this.getConnectionArgs(false),
|
|
471
|
+
"-F",
|
|
472
|
+
"c",
|
|
473
|
+
"-b",
|
|
474
|
+
"--quote-all-identifiers",
|
|
475
|
+
...backupSchema ? [`--schema=${backupSchema}`] : [],
|
|
476
|
+
"-f",
|
|
477
|
+
filePath,
|
|
478
|
+
database
|
|
479
|
+
];
|
|
480
|
+
await runSpawn(this.getBackupCommandName(), args, this.getPasswordEnvVars(password));
|
|
357
481
|
}
|
|
358
|
-
async
|
|
359
|
-
|
|
482
|
+
async getOwnedSequenceTables(excludeTables, backupSchema) {
|
|
483
|
+
if (!Array.isArray(excludeTables) || !excludeTables.length) {
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
const tableRefs = excludeTables.map((table) => parsePgTableReference(table, backupSchema)).filter((ref) => ref.table);
|
|
487
|
+
if (!tableRefs.length) {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
const values = tableRefs.map((ref) => {
|
|
491
|
+
const schemaValue = ref.schema == null ? "NULL" : `'${escapeStringLiteral(ref.schema)}'`;
|
|
492
|
+
return `(${schemaValue}, '${escapeStringLiteral(ref.table)}')`;
|
|
493
|
+
}).join(",");
|
|
494
|
+
const query = `
|
|
495
|
+
WITH excluded(schema_name, table_name) AS (VALUES ${values})
|
|
496
|
+
SELECT seq_ns.nspname || '.' || seq.relname
|
|
497
|
+
FROM excluded
|
|
498
|
+
JOIN pg_class tbl ON tbl.relname = excluded.table_name
|
|
499
|
+
JOIN pg_namespace tbl_ns ON tbl_ns.oid = tbl.relnamespace
|
|
500
|
+
JOIN pg_depend dep ON dep.refobjid = tbl.oid
|
|
501
|
+
JOIN pg_class seq ON seq.oid = dep.objid AND seq.relkind = 'S'
|
|
502
|
+
JOIN pg_namespace seq_ns ON seq_ns.oid = seq.relnamespace
|
|
503
|
+
WHERE tbl.relkind IN ('r', 'p')
|
|
504
|
+
AND dep.deptype IN ('a', 'i')
|
|
505
|
+
AND (excluded.schema_name IS NULL OR tbl_ns.nspname = excluded.schema_name)
|
|
506
|
+
`;
|
|
507
|
+
const output = String(await this.runSql(query, ["-At"]));
|
|
508
|
+
return output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
509
|
+
}
|
|
510
|
+
getConnectionArgs(includeDatabase = true) {
|
|
511
|
+
const { username, host, port, database } = this.dbOpts;
|
|
512
|
+
return [
|
|
513
|
+
"-U",
|
|
514
|
+
username,
|
|
515
|
+
"-h",
|
|
516
|
+
host,
|
|
517
|
+
...port ? ["-p", String(port)] : [],
|
|
518
|
+
...includeDatabase ? ["-d", database] : []
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
runSql(query, extraArgs = [], toolchain = this.backupToolchain) {
|
|
522
|
+
const { password } = this.dbOpts;
|
|
523
|
+
return runSpawn(
|
|
524
|
+
this.getSqlCommandName(toolchain),
|
|
525
|
+
[...this.getConnectionArgs(), ...extraArgs, "-c", query],
|
|
526
|
+
this.getPasswordEnvVars(password, toolchain)
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
runRestore(args, toolchain = this.backupToolchain) {
|
|
530
|
+
const { password } = this.dbOpts;
|
|
531
|
+
return runSpawn(this.getRestoreCommandName(toolchain), args, this.getPasswordEnvVars(password, toolchain));
|
|
532
|
+
}
|
|
533
|
+
async restore({
|
|
534
|
+
filePath,
|
|
535
|
+
schema,
|
|
536
|
+
skipDropAllTables = false,
|
|
537
|
+
restoreMode,
|
|
538
|
+
toolchain = this.backupToolchain
|
|
539
|
+
}) {
|
|
360
540
|
let schemaOption = this.dbOpts.schema;
|
|
361
541
|
if (schema && !schemaOption) {
|
|
362
542
|
schemaOption = "public";
|
|
363
543
|
}
|
|
544
|
+
if (schema) {
|
|
545
|
+
assertPostgresSchemaIdentifier(schema);
|
|
546
|
+
}
|
|
547
|
+
if (schemaOption) {
|
|
548
|
+
assertPostgresSchemaIdentifier(schemaOption);
|
|
549
|
+
}
|
|
364
550
|
const cpuCores = import_os.default.cpus().length;
|
|
365
551
|
const j = Math.max(1, Math.floor(cpuCores / 2));
|
|
366
552
|
const schemaNameCondition = schemaOption ? `WHERE schemaname = '${schemaOption}'` : `WHERE schemaname NOT IN ('pg_catalog', 'information_schema')`;
|
|
367
553
|
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
|
|
370
|
-
DO
|
|
554
|
+
if (restoreMode === "preserveTables") {
|
|
555
|
+
const dropViewsQuery = `
|
|
556
|
+
DO $$ DECLARE r RECORD;
|
|
557
|
+
BEGIN
|
|
558
|
+
FOR r IN (
|
|
559
|
+
SELECT schemaname, viewname, false AS materialized FROM pg_views ${schemaNameCondition}
|
|
560
|
+
UNION ALL
|
|
561
|
+
SELECT schemaname, matviewname AS viewname, true AS materialized FROM pg_matviews ${schemaNameCondition}
|
|
562
|
+
) LOOP
|
|
563
|
+
BEGIN
|
|
564
|
+
IF r.materialized THEN
|
|
565
|
+
EXECUTE 'DROP MATERIALIZED VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.viewname) || ' CASCADE';
|
|
566
|
+
ELSE
|
|
567
|
+
EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.viewname) || ' CASCADE';
|
|
568
|
+
END IF;
|
|
569
|
+
EXCEPTION
|
|
570
|
+
WHEN OTHERS THEN
|
|
571
|
+
END;
|
|
572
|
+
END LOOP;
|
|
573
|
+
END $$;`;
|
|
574
|
+
await this.runSql(dropViewsQuery, [], toolchain);
|
|
575
|
+
} else if (!skipDropAllTables) {
|
|
576
|
+
const dropDataQuery = `
|
|
577
|
+
DO $$ DECLARE r RECORD;
|
|
371
578
|
BEGIN
|
|
372
579
|
FOR r IN (SELECT viewname,schemaname FROM pg_views ${schemaNameCondition}) LOOP
|
|
373
580
|
BEGIN
|
|
374
|
-
EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.
|
|
581
|
+
EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.viewname) || ' CASCADE';
|
|
375
582
|
EXCEPTION
|
|
376
583
|
WHEN OTHERS THEN
|
|
377
584
|
END;
|
|
@@ -387,7 +594,7 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
387
594
|
|
|
388
595
|
FOR r IN (SELECT sequencename,schemaname FROM pg_sequences ${schemaNameCondition}) LOOP
|
|
389
596
|
BEGIN
|
|
390
|
-
EXECUTE 'DROP SEQUENCE IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.
|
|
597
|
+
EXECUTE 'DROP SEQUENCE IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.sequencename) || ' CASCADE';
|
|
391
598
|
EXCEPTION
|
|
392
599
|
WHEN OTHERS THEN
|
|
393
600
|
END;
|
|
@@ -405,54 +612,95 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
405
612
|
END;
|
|
406
613
|
END LOOP;
|
|
407
614
|
|
|
408
|
-
END
|
|
409
|
-
await
|
|
615
|
+
END $$;`;
|
|
616
|
+
await this.runSql(dropDataQuery, [], toolchain);
|
|
410
617
|
}
|
|
411
618
|
if (schema === schemaOption || !schemaOption) {
|
|
412
|
-
const
|
|
413
|
-
|
|
619
|
+
const restoreList = restoreMode === "preserveTables" ? await this.createRestoreListWithoutSchema(filePath, toolchain) : void 0;
|
|
620
|
+
const restoreArgs = [
|
|
621
|
+
...this.getConnectionArgs(),
|
|
622
|
+
"--clean",
|
|
623
|
+
"--if-exists",
|
|
624
|
+
"--no-owner",
|
|
625
|
+
"-j",
|
|
626
|
+
String(j),
|
|
627
|
+
...restoreList ? ["-L", restoreList.filePath] : [],
|
|
628
|
+
filePath
|
|
629
|
+
];
|
|
630
|
+
try {
|
|
631
|
+
await this.runRestore(restoreArgs, toolchain);
|
|
632
|
+
} finally {
|
|
633
|
+
if (restoreList) {
|
|
634
|
+
await fsPromises.unlink(restoreList.filePath).catch(() => {
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
414
638
|
} else {
|
|
415
639
|
const srcSchema = schema || "public";
|
|
416
|
-
const
|
|
417
|
-
await this
|
|
640
|
+
const pgRestoreArgs = this.buildSchemaRestoreArgs(srcSchema, schemaOption, filePath, j, toolchain);
|
|
641
|
+
await this.restoreSchema(srcSchema, schemaOption, pgRestoreArgs, toolchain);
|
|
418
642
|
}
|
|
419
643
|
}
|
|
420
|
-
async
|
|
421
|
-
const
|
|
644
|
+
async createRestoreListWithoutSchema(filePath, toolchain = this.backupToolchain) {
|
|
645
|
+
const listOutput = String(await this.runRestore(["--list", filePath], toolchain));
|
|
646
|
+
const filteredList = listOutput.split("\n").filter((line) => !isPgRestoreSchemaTocEntry(line)).join("\n");
|
|
647
|
+
const listFilePath = import_path.default.join(
|
|
648
|
+
import_os.default.tmpdir(),
|
|
649
|
+
`nocobase-pg-restore-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.list`
|
|
650
|
+
);
|
|
651
|
+
await fsPromises.writeFile(listFilePath, filteredList);
|
|
652
|
+
return {
|
|
653
|
+
filePath: listFilePath
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
buildSchemaRestoreArgs(srcSchema, _targetSchema, filePath, jobs, _toolchain = this.backupToolchain) {
|
|
657
|
+
return [
|
|
658
|
+
...this.getConnectionArgs(),
|
|
659
|
+
"-n",
|
|
660
|
+
srcSchema,
|
|
661
|
+
"--clean",
|
|
662
|
+
"--if-exists",
|
|
663
|
+
"--no-owner",
|
|
664
|
+
"-j",
|
|
665
|
+
String(jobs),
|
|
666
|
+
filePath
|
|
667
|
+
];
|
|
668
|
+
}
|
|
669
|
+
async restoreSchema(srcSchema, targetSchema, pgRestoreArgs, toolchain = this.backupToolchain) {
|
|
422
670
|
const ts = Date.now();
|
|
423
|
-
const
|
|
424
|
-
DO
|
|
671
|
+
const preQuery = `
|
|
672
|
+
DO $$
|
|
425
673
|
BEGIN
|
|
426
674
|
IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = '${srcSchema}') THEN
|
|
427
675
|
EXECUTE 'ALTER SCHEMA ${srcSchema} RENAME TO ${srcSchema}_${ts}';
|
|
428
676
|
END IF;
|
|
429
677
|
EXECUTE 'CREATE SCHEMA ${srcSchema}';
|
|
430
|
-
END
|
|
431
|
-
const
|
|
432
|
-
DO
|
|
678
|
+
END $$;`;
|
|
679
|
+
const postQuery = `
|
|
680
|
+
DO $$
|
|
433
681
|
BEGIN
|
|
434
682
|
EXECUTE 'DROP SCHEMA ${targetSchema} CASCADE';
|
|
435
683
|
EXECUTE 'ALTER SCHEMA ${srcSchema} RENAME TO ${targetSchema}';
|
|
436
684
|
IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = '${srcSchema}_${ts}') THEN
|
|
437
685
|
EXECUTE 'ALTER SCHEMA ${srcSchema}_${ts} RENAME TO ${srcSchema}';
|
|
438
686
|
END IF;
|
|
439
|
-
END
|
|
440
|
-
await
|
|
687
|
+
END $$;`;
|
|
688
|
+
await this.runSql(preQuery, [], toolchain);
|
|
441
689
|
try {
|
|
442
|
-
await
|
|
690
|
+
await this.runRestore(pgRestoreArgs, toolchain);
|
|
443
691
|
} finally {
|
|
444
|
-
await
|
|
692
|
+
await this.runSql(postQuery, [], toolchain);
|
|
445
693
|
}
|
|
446
|
-
await this
|
|
694
|
+
await this.syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain);
|
|
447
695
|
}
|
|
448
|
-
async
|
|
449
|
-
const {
|
|
696
|
+
async syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain = this.backupToolchain) {
|
|
697
|
+
const { tablePrefix } = this.dbOpts;
|
|
450
698
|
const collectionsTable = `${tablePrefix || ""}collections`;
|
|
451
699
|
const targetSchemaLiteral = escapeStringLiteral(targetSchema);
|
|
452
700
|
const srcSchemaLiteral = escapeStringLiteral(srcSchema);
|
|
453
701
|
const collectionsTableLiteral = escapeStringLiteral(collectionsTable);
|
|
454
|
-
const
|
|
455
|
-
DO
|
|
702
|
+
const updateCollectionSchemaQuery = `
|
|
703
|
+
DO $$
|
|
456
704
|
DECLARE
|
|
457
705
|
collections_table text := '${collectionsTableLiteral}';
|
|
458
706
|
BEGIN
|
|
@@ -470,8 +718,71 @@ class PostgresAdapter extends BaseDBAdapter {
|
|
|
470
718
|
'${srcSchemaLiteral}'
|
|
471
719
|
);
|
|
472
720
|
END IF;
|
|
473
|
-
END
|
|
474
|
-
await
|
|
721
|
+
END $$;`;
|
|
722
|
+
await this.runSql(updateCollectionSchemaQuery, [], toolchain);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
class KingbaseAdapter extends PostgresAdapter {
|
|
726
|
+
get backupToolchain() {
|
|
727
|
+
return this.hasKingbaseToolchain() ? "kingbase" : "postgres";
|
|
728
|
+
}
|
|
729
|
+
getKingbaseBackupCommandName() {
|
|
730
|
+
return formatPathInEnv(process.env.KINGBASE_DUMP_PATH) || "sys_dump";
|
|
731
|
+
}
|
|
732
|
+
getKingbaseRestoreCommandName() {
|
|
733
|
+
return formatPathInEnv(process.env.KINGBASE_RESTORE_PATH) || "sys_restore";
|
|
734
|
+
}
|
|
735
|
+
getKingbaseSqlCommandName() {
|
|
736
|
+
return formatPathInEnv(process.env.KINGBASE_PSQL_PATH) || "ksql";
|
|
737
|
+
}
|
|
738
|
+
hasKingbaseToolchain() {
|
|
739
|
+
return this.hasCommand(this.getKingbaseBackupCommandName()) && this.hasCommand(this.getKingbaseRestoreCommandName()) && this.hasCommand(this.getKingbaseSqlCommandName());
|
|
740
|
+
}
|
|
741
|
+
getBackupCommandName(toolchain = this.backupToolchain) {
|
|
742
|
+
return toolchain === "kingbase" ? this.getKingbaseBackupCommandName() : super.getBackupCommandName(toolchain);
|
|
743
|
+
}
|
|
744
|
+
getRestoreCommandName(toolchain = this.backupToolchain) {
|
|
745
|
+
return toolchain === "kingbase" ? this.getKingbaseRestoreCommandName() : super.getRestoreCommandName(toolchain);
|
|
746
|
+
}
|
|
747
|
+
getSqlCommandName(toolchain = this.backupToolchain) {
|
|
748
|
+
return toolchain === "kingbase" ? this.getKingbaseSqlCommandName() : super.getSqlCommandName(toolchain);
|
|
749
|
+
}
|
|
750
|
+
getPasswordEnvVars(password, toolchain = this.backupToolchain) {
|
|
751
|
+
if (toolchain === "postgres") {
|
|
752
|
+
return super.getPasswordEnvVars(password, toolchain);
|
|
753
|
+
}
|
|
754
|
+
return { KINGBASE_PASSWORD: password };
|
|
755
|
+
}
|
|
756
|
+
getBackupSchema() {
|
|
757
|
+
return this.dbOpts.schema || "public";
|
|
758
|
+
}
|
|
759
|
+
buildSchemaRestoreArgs(srcSchema, targetSchema, filePath, jobs, toolchain = this.backupToolchain) {
|
|
760
|
+
if (toolchain === "postgres") {
|
|
761
|
+
return super.buildSchemaRestoreArgs(srcSchema, targetSchema, filePath, jobs, toolchain);
|
|
762
|
+
}
|
|
763
|
+
return [
|
|
764
|
+
...this.getConnectionArgs(),
|
|
765
|
+
"-g",
|
|
766
|
+
srcSchema,
|
|
767
|
+
"-G",
|
|
768
|
+
targetSchema,
|
|
769
|
+
"--clean",
|
|
770
|
+
"--if-exists",
|
|
771
|
+
"--no-owner",
|
|
772
|
+
"-j",
|
|
773
|
+
String(jobs),
|
|
774
|
+
filePath
|
|
775
|
+
];
|
|
776
|
+
}
|
|
777
|
+
async restoreSchema(srcSchema, targetSchema, restoreArgs, toolchain = this.backupToolchain) {
|
|
778
|
+
if (toolchain === "postgres") {
|
|
779
|
+
await super.restoreSchema(srcSchema, targetSchema, restoreArgs, toolchain);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const targetSchemaIdentifier = quotePgIdentifier(targetSchema);
|
|
783
|
+
await this.runSql(`CREATE SCHEMA IF NOT EXISTS ${targetSchemaIdentifier};`, [], toolchain);
|
|
784
|
+
await this.runRestore(restoreArgs, toolchain);
|
|
785
|
+
await this.syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain);
|
|
475
786
|
}
|
|
476
787
|
}
|
|
477
788
|
class SQLiteAdapter extends BaseDBAdapter {
|
|
@@ -492,6 +803,7 @@ class MariaDBAdapter extends MySQLAdapter {
|
|
|
492
803
|
const adapterMap = {
|
|
493
804
|
mysql: MySQLAdapter,
|
|
494
805
|
postgres: PostgresAdapter,
|
|
806
|
+
kingbase: KingbaseAdapter,
|
|
495
807
|
sqlite: SQLiteAdapter,
|
|
496
808
|
mariadb: MariaDBAdapter
|
|
497
809
|
};
|
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
|
});
|
|
@@ -11,16 +11,20 @@ import { ResourcerContext } from '@nocobase/resourcer';
|
|
|
11
11
|
import Application from '@nocobase/server';
|
|
12
12
|
import { Readable } from 'stream';
|
|
13
13
|
export interface BackupSettings {
|
|
14
|
-
storageId?: string;
|
|
14
|
+
storageId?: string | number | null;
|
|
15
15
|
encryptionPassword: string;
|
|
16
16
|
enableFilesBackup: boolean;
|
|
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;
|
|
@@ -38,11 +42,14 @@ export interface BackupTaskResult {
|
|
|
38
42
|
message?: string;
|
|
39
43
|
inProgress: boolean;
|
|
40
44
|
}
|
|
45
|
+
type BackupSettingsInput = BackupSettings & {
|
|
46
|
+
toJSON?: () => Partial<BackupSettings>;
|
|
47
|
+
};
|
|
41
48
|
export declare class BackupManager {
|
|
42
49
|
#private;
|
|
43
50
|
app: Application;
|
|
44
51
|
ctx: ResourcerContext | null;
|
|
45
|
-
constructor(app: Application, ctx: ResourcerContext | null, settings:
|
|
52
|
+
constructor(app: Application, ctx: ResourcerContext | null, settings: BackupSettingsInput);
|
|
46
53
|
protected set backupPrefix(backupPrefix: string);
|
|
47
54
|
protected set backupDir(backupDir: string);
|
|
48
55
|
protected set tempDir(tempDir: string);
|
|
@@ -54,3 +61,4 @@ export declare class BackupManager {
|
|
|
54
61
|
list(): Promise<BackupFile[]>;
|
|
55
62
|
createReadStream(fileName: string): Promise<Readable>;
|
|
56
63
|
}
|
|
64
|
+
export {};
|