@nocobase/plugin-backups 2.2.0-beta.1 → 2.2.0-beta.11

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.
Files changed (41) hide show
  1. package/dist/client/components/RestoreFromBackup.d.ts +8 -0
  2. package/dist/client/components/RestoreFromLocal.d.ts +8 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/client-v2/{105.064f69093e98a2dd.js → 105.d20cb8a299876029.js} +1 -1
  5. package/dist/client-v2/567.ab673b2df222e1c4.js +10 -0
  6. package/dist/client-v2/index.js +1 -1
  7. package/dist/externalVersion.js +12 -11
  8. package/dist/locale/de-DE.json +1 -1
  9. package/dist/locale/en-US.json +1 -1
  10. package/dist/locale/es-ES.json +1 -1
  11. package/dist/locale/fr-FR.json +1 -1
  12. package/dist/locale/hu-HU.json +1 -1
  13. package/dist/locale/id-ID.json +1 -1
  14. package/dist/locale/it-IT.json +1 -1
  15. package/dist/locale/ja-JP.json +1 -1
  16. package/dist/locale/ko-KR.json +1 -1
  17. package/dist/locale/nl-NL.json +1 -1
  18. package/dist/locale/pt-BR.json +1 -1
  19. package/dist/locale/ru-RU.json +1 -1
  20. package/dist/locale/tr-TR.json +1 -1
  21. package/dist/locale/uk-UA.json +1 -1
  22. package/dist/locale/vi-VN.json +1 -1
  23. package/dist/locale/zh-CN.json +1 -1
  24. package/dist/locale/zh-TW.json +1 -1
  25. package/dist/node_modules/archiver/package.json +1 -1
  26. package/dist/node_modules/cron-parser/package.json +1 -1
  27. package/dist/node_modules/fs-extra/package.json +1 -1
  28. package/dist/node_modules/semver/package.json +1 -1
  29. package/dist/node_modules/yauzl/package.json +1 -1
  30. package/dist/server/adapters/database.d.ts +7 -0
  31. package/dist/server/adapters/database.js +368 -56
  32. package/dist/server/index.d.ts +1 -1
  33. package/dist/server/index.js +7 -1
  34. package/dist/server/managers/backup.d.ts +10 -2
  35. package/dist/server/managers/backup.js +14 -3
  36. package/dist/server/managers/restore.d.ts +1 -0
  37. package/dist/server/managers/restore.js +27 -4
  38. package/dist/server/utils.d.ts +1 -0
  39. package/dist/server/utils.js +29 -1
  40. package/package.json +2 -2
  41. 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 { stdout } = await exec(command, { env: { ...process.env, ...envVars } });
56
- return stdout;
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 && /\s/.test(path2) && !/^".*"$/.test(path2) && !/^'.*'$/.test(path2)) {
63
- return `"${path2}"`;
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 quoteShellArg = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;
70
- const quotePgTablePattern = (table) => quoteShellArg(String(table).split(".").map(quotePgIdentifier).join("."));
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
- try {
88
- (0, import_child_process.execSync)(`${command} --version`);
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/handbook/backups#installation`
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 (!skipDropAllTables) {
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 #backupCmd() {
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
- get #restoreCmd() {
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.#backupCmd);
442
+ this.assertCommand(this.getBackupCommandName());
335
443
  break;
336
444
  case "restore":
337
- this.assertCommand(this.#restoreCmd);
445
+ this.assertCommand(this.getRestoreCommandName());
338
446
  break;
339
447
  }
340
448
  }
341
449
  async clientVersion(op) {
342
- const cmd = op === "backup" ? this.#backupCmd : this.#restoreCmd;
450
+ const cmd = op === "backup" ? this.getBackupCommandName() : this.getRestoreCommandName();
343
451
  try {
344
- return (0, import_child_process.execSync)(`${cmd} --version`).toString();
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 { username, host, port, database, password, schema: backupSchema } = this.dbOpts;
458
+ const { database, password } = this.dbOpts;
351
459
  const filePath = `${dir}/data`;
352
- const schemaOption = backupSchema ? `--schema=${backupSchema}` : "";
353
- const includeOption = Array.isArray(includeTables) && includeTables.length ? includeTables.map((table) => `-t ${quotePgTablePattern(qualifyPgTablePattern(table, backupSchema))}`).join(" ") : "";
354
- const excludeOption = Array.isArray(excludeTables) && excludeTables.length ? excludeTables.map((table) => `-T ${quotePgTablePattern(qualifyPgTablePattern(table, backupSchema))}`).join(" ") : "";
355
- const command = `${this.#backupCmd} ${includeOption} ${excludeOption} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -F c -b --quote-all-identifiers ${schemaOption} -f ${filePath} ${database}`;
356
- await run(command, { PGPASSWORD: password });
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 restore({ filePath, schema, skipDropAllTables = false }) {
359
- const { username, host, port, database, password } = this.dbOpts;
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 (!skipDropAllTables) {
369
- const dropDataCommand = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
370
- DO ${D$$} DECLARE r RECORD;
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.tablename) || ' CASCADE';
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.tablename) || ' CASCADE';
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 ${D$$};"`.replace(/\n/g, " ");
409
- await run(dropDataCommand, { PGPASSWORD: password });
615
+ END $$;`;
616
+ await this.runSql(dropDataQuery, [], toolchain);
410
617
  }
411
618
  if (schema === schemaOption || !schemaOption) {
412
- const pgRestoreCommand = `${this.#restoreCmd} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} --clean --if-exists --no-owner -j ${j} ${filePath}`;
413
- await run(pgRestoreCommand, { PGPASSWORD: password });
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 pgRestoreCommand = `${this.#restoreCmd} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -n ${srcSchema} -d ${database} --clean --if-exists --no-owner -j ${j} ${filePath}`;
417
- await this.#restoreSchema(srcSchema, schemaOption, pgRestoreCommand);
640
+ const pgRestoreArgs = this.buildSchemaRestoreArgs(srcSchema, schemaOption, filePath, j, toolchain);
641
+ await this.restoreSchema(srcSchema, schemaOption, pgRestoreArgs, toolchain);
418
642
  }
419
643
  }
420
- async #restoreSchema(srcSchema, targetSchema, pgRestoreCommand) {
421
- const { username, host, port, database, password } = this.dbOpts;
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 preCommand = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
424
- DO ${D$$}
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 ${D$$};"`.replace(/\n/g, " ");
431
- const postCommand = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
432
- DO ${D$$}
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 ${D$$};"`.replace(/\n/g, " ");
440
- await run(preCommand, { PGPASSWORD: password });
687
+ END $$;`;
688
+ await this.runSql(preQuery, [], toolchain);
441
689
  try {
442
- await run(pgRestoreCommand, { PGPASSWORD: password });
690
+ await this.runRestore(pgRestoreArgs, toolchain);
443
691
  } finally {
444
- await run(postCommand, { PGPASSWORD: password });
692
+ await this.runSql(postQuery, [], toolchain);
445
693
  }
446
- await this.#syncCollectionSchemaMetadata(srcSchema, targetSchema);
694
+ await this.syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain);
447
695
  }
448
- async #syncCollectionSchemaMetadata(srcSchema, targetSchema) {
449
- const { username, host, port, database, password, tablePrefix } = this.dbOpts;
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 updateCollectionSchemaCommand = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
455
- DO ${D$$}
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 ${D$$};"`.replace(/\n/g, " ");
474
- await run(updateCollectionSchemaCommand, { PGPASSWORD: password });
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
  };
@@ -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';
@@ -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: BackupSettings);
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 {};