@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.
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.942d36050408575f.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 +265 -34
  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 +4 -0
  35. package/dist/server/managers/backup.js +7 -2
  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
@@ -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 { stdout } = await exec(command, { env: { ...process.env, ...envVars } });
56
- return stdout;
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/handbook/backups#installation`
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 (!skipDropAllTables) {
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 #backupCmd() {
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
- get #restoreCmd() {
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.#backupCmd);
419
+ this.assertCommand(this.getBackupCommandName());
335
420
  break;
336
421
  case "restore":
337
- this.assertCommand(this.#restoreCmd);
422
+ this.assertCommand(this.getRestoreCommandName());
338
423
  break;
339
424
  }
340
425
  }
341
426
  async clientVersion(op) {
342
- const cmd = op === "backup" ? this.#backupCmd : this.#restoreCmd;
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, schema: backupSchema } = this.dbOpts;
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 = 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 });
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 restore({ filePath, schema, skipDropAllTables = false }) {
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 (!skipDropAllTables) {
369
- const dropDataCommand = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
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.tablename) || ' CASCADE';
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.tablename) || ' CASCADE';
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, { PGPASSWORD: password });
559
+ await run(dropDataCommand, this.getPasswordEnvVars(password, toolchain));
410
560
  }
411
561
  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 });
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 = `${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);
574
+ const pgRestoreCommand = this.buildSchemaRestoreCommand(srcSchema, schemaOption, filePath, j, toolchain);
575
+ await this.restoreSchema(srcSchema, schemaOption, pgRestoreCommand, toolchain);
418
576
  }
419
577
  }
420
- async #restoreSchema(srcSchema, targetSchema, pgRestoreCommand) {
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 preCommand = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
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 = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
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, { PGPASSWORD: password });
616
+ await run(preCommand, this.getPasswordEnvVars(password, toolchain));
441
617
  try {
442
- await run(pgRestoreCommand, { PGPASSWORD: password });
618
+ await run(pgRestoreCommand, this.getPasswordEnvVars(password, toolchain));
443
619
  } finally {
444
- await run(postCommand, { PGPASSWORD: password });
620
+ await run(postCommand, this.getPasswordEnvVars(password, toolchain));
445
621
  }
446
- await this.#syncCollectionSchemaMetadata(srcSchema, targetSchema);
622
+ await this.syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain);
447
623
  }
448
- async #syncCollectionSchemaMetadata(srcSchema, targetSchema) {
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 = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
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, { PGPASSWORD: password });
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
  };
@@ -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
  });
@@ -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 = 1;
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
- return this.#runBackupTask({ ...this.#settings, ...opts ?? {} }, fileBaseName, contentPath);
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,
@@ -11,6 +11,7 @@ import { ResourcerContext } from '@nocobase/resourcer';
11
11
  export interface RestoreOptions {
12
12
  forceSchemaRestore?: boolean;
13
13
  skipDropAllTables?: boolean;
14
+ restoreMode?: 'preserveTables';
14
15
  }
15
16
  export declare class RestoreManager {
16
17
  #private;
@@ -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 === "postgres") {
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 === "postgres";
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) {
@@ -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;