@webtypen/webframez-core 0.3.55 → 0.3.56

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.
@@ -1,4 +1,4 @@
1
- import { BackupCleanupOptions, BackupCleanupResult, BackupConfig, BackupOutputConfig, BackupRetentionConfig, BackupRunOptions, BackupRunResult, BackupTypeConfig } from "./BackupTypes";
1
+ import { BackupCleanupOptions, BackupCleanupResult, BackupConfig, BackupOutputConfig, BackupRetentionConfig, BackupRestorePoint, BackupRestoreResult, BackupRunOptions, BackupRunResult, BackupTypeConfig } from "./BackupTypes";
2
2
  type ResolvedBackupTypeConfig = BackupTypeConfig & {
3
3
  workDir: string;
4
4
  outputDir: string;
@@ -24,9 +24,29 @@ export declare class BackupManager {
24
24
  private resolveOutputs;
25
25
  private collectFilesFromSource;
26
26
  private copyFiles;
27
+ private buildFileIndex;
28
+ private fileIndexChanged;
29
+ private incrementalEnabled;
30
+ private assertIncrementalSupported;
31
+ private chooseOutputForRead;
32
+ private listRestorePointsForOutput;
33
+ listRestorePoints(key: string, options?: {
34
+ channels?: string[];
35
+ }): Promise<BackupRestorePoint[]>;
36
+ private isFullIncrementalRun;
37
+ private buildBackupManifest;
38
+ private writeContentManifest;
27
39
  private backupDatabase;
28
40
  private buildArtifact;
29
41
  run(key: string, options?: BackupRunOptions): Promise<BackupRunResult>;
42
+ private downloadRestoreArtifact;
43
+ private restoreChain;
44
+ private extractArtifact;
45
+ restore(key: string, targetDir: string | undefined, options?: {
46
+ backupId?: string;
47
+ channels?: string[];
48
+ dryRun?: boolean;
49
+ }): Promise<BackupRestoreResult>;
30
50
  cleanup(key: string, options?: BackupCleanupOptions): Promise<BackupCleanupResult[]>;
31
51
  }
32
52
  export {};
@@ -15,12 +15,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.BackupManager = void 0;
16
16
  const fs_1 = __importDefault(require("fs"));
17
17
  const path_1 = __importDefault(require("path"));
18
+ const os_1 = __importDefault(require("os"));
19
+ const crypto_1 = __importDefault(require("crypto"));
20
+ const extract_zip_1 = __importDefault(require("extract-zip"));
18
21
  const Config_1 = require("../Config");
19
22
  const DBConnection_1 = require("../Database/DBConnection");
20
23
  const BackupOutputDrivers_1 = require("./BackupOutputDrivers");
21
24
  const BackupPathUtils_1 = require("./Utils/BackupPathUtils");
22
25
  const GlobMatcher_1 = require("./Utils/GlobMatcher");
23
26
  const ZipWriter_1 = require("./Utils/ZipWriter");
27
+ const AutomationSchedule_1 = require("../Queue/AutomationSchedule");
24
28
  function statSize(filepath) {
25
29
  const stats = fs_1.default.statSync(filepath);
26
30
  if (!stats.isDirectory()) {
@@ -39,6 +43,47 @@ function mergeRetention(defaults, typeRetention, outputRetention) {
39
43
  function normalizeArray(value) {
40
44
  return value && Array.isArray(value) ? value : [];
41
45
  }
46
+ function safeReadJson(filepath) {
47
+ try {
48
+ return JSON.parse(fs_1.default.readFileSync(filepath, "utf-8"));
49
+ }
50
+ catch (e) {
51
+ return null;
52
+ }
53
+ }
54
+ function sha256File(filepath) {
55
+ const hash = crypto_1.default.createHash("sha256");
56
+ hash.update(fs_1.default.readFileSync(filepath));
57
+ return hash.digest("hex");
58
+ }
59
+ function restorePointCreatedAt(point) {
60
+ return point.createdAt instanceof Date ? point.createdAt : new Date(point.createdAt);
61
+ }
62
+ function normalizeManifest(input, entry) {
63
+ var _a, _b, _c, _d, _e, _f, _g;
64
+ if (!input) {
65
+ return null;
66
+ }
67
+ const manifest = input.backupManifest || input;
68
+ const backupKey = manifest.backupKey || input.key || (entry === null || entry === void 0 ? void 0 : entry.backupKey);
69
+ const backupId = manifest.backupId || input.id || (entry === null || entry === void 0 ? void 0 : entry.backupId);
70
+ if (!backupKey || !backupId) {
71
+ return null;
72
+ }
73
+ return Object.assign(Object.assign({}, manifest), { backupKey: backupKey, backupId: backupId, kind: manifest.kind || input.kind || "normal", chainId: manifest.chainId || input.chainId, parentBackupId: manifest.parentBackupId || input.parentBackupId, createdAt: manifest.createdAt || input.startedAt || ((_b = (_a = entry === null || entry === void 0 ? void 0 : entry.createdAt) === null || _a === void 0 ? void 0 : _a.toISOString) === null || _b === void 0 ? void 0 : _b.call(_a)) || new Date().toISOString(), artifact: manifest.artifact || ((_c = input.artifact) === null || _c === void 0 ? void 0 : _c.filename) || input.artifact || (entry === null || entry === void 0 ? void 0 : entry.filename), artifactType: manifest.artifactType || ((_d = input.artifact) === null || _d === void 0 ? void 0 : _d.type) || "zip", files: {
74
+ upserted: ((_e = manifest.files) === null || _e === void 0 ? void 0 : _e.upserted) || [],
75
+ deleted: ((_f = manifest.files) === null || _f === void 0 ? void 0 : _f.deleted) || [],
76
+ fileIndex: ((_g = manifest.files) === null || _g === void 0 ? void 0 : _g.fileIndex) || {},
77
+ } });
78
+ }
79
+ function assertInside(targetRoot, relative) {
80
+ const target = path_1.default.resolve(targetRoot, relative);
81
+ const root = path_1.default.resolve(targetRoot);
82
+ if (target !== root && !target.startsWith(root + path_1.default.sep)) {
83
+ throw new Error(`Refusing to restore unsafe path '${relative}'.`);
84
+ }
85
+ return target;
86
+ }
42
87
  class BackupManager {
43
88
  log(options, message, payload) {
44
89
  if ((options === null || options === void 0 ? void 0 : options.silent) || !(options === null || options === void 0 ? void 0 : options.log)) {
@@ -150,11 +195,13 @@ class BackupManager {
150
195
  return;
151
196
  }
152
197
  included += 1;
198
+ const stats = fs_1.default.statSync(filepath);
153
199
  files.push({
154
200
  source: filepath,
155
201
  relative: relative,
156
202
  target: (0, BackupPathUtils_1.normalizeBackupPath)(path_1.default.join(toBase, relative)),
157
- size: fs_1.default.statSync(filepath).size,
203
+ size: stats.size,
204
+ mtimeMs: stats.mtimeMs,
158
205
  });
159
206
  if (visited % interval === 0) {
160
207
  this.log(options, `Scanned ${visited} file(s), included ${included}`);
@@ -193,6 +240,160 @@ class BackupManager {
193
240
  }
194
241
  }
195
242
  }
243
+ buildFileIndex(files, checksum) {
244
+ const index = {};
245
+ for (const file of files) {
246
+ index[file.target] = Object.assign({ path: file.target, size: file.size, mtimeMs: Math.round(file.mtimeMs) }, (checksum ? { checksum: sha256File(file.source) } : {}));
247
+ }
248
+ return index;
249
+ }
250
+ fileIndexChanged(current, previous, checksum) {
251
+ if (!previous) {
252
+ return true;
253
+ }
254
+ if (current.size !== previous.size) {
255
+ return true;
256
+ }
257
+ if (checksum) {
258
+ return current.checksum !== previous.checksum;
259
+ }
260
+ return Math.round(current.mtimeMs || 0) !== Math.round(previous.mtimeMs || 0);
261
+ }
262
+ incrementalEnabled(backupType) {
263
+ var _a;
264
+ return ((_a = backupType.incremental) === null || _a === void 0 ? void 0 : _a.enabled) === true;
265
+ }
266
+ assertIncrementalSupported(key, backupType) {
267
+ if (!this.incrementalEnabled(backupType)) {
268
+ return;
269
+ }
270
+ if (!backupType.zip) {
271
+ throw new Error(`Incremental backup '${key}' requires zip=true.`);
272
+ }
273
+ if (normalizeArray(backupType.databases).length > 0) {
274
+ throw new Error(`Incremental backup '${key}' supports file-only backup types in v1.`);
275
+ }
276
+ }
277
+ chooseOutputForRead(backupType, channels) {
278
+ const outputs = this.resolveOutputs(backupType, channels);
279
+ if (outputs.length < 1) {
280
+ return null;
281
+ }
282
+ return outputs[0];
283
+ }
284
+ listRestorePointsForOutput(key, output) {
285
+ return __awaiter(this, void 0, void 0, function* () {
286
+ const driver = BackupOutputDrivers_1.BackupOutputDrivers.get(output.driver);
287
+ if (!driver.listArtifacts || !driver.readManifest) {
288
+ return [];
289
+ }
290
+ const entries = yield driver.listArtifacts(output, {
291
+ backupKey: key,
292
+ backupId: (0, BackupPathUtils_1.backupTimestampId)(),
293
+ log: () => null,
294
+ });
295
+ const points = [];
296
+ for (const entry of entries) {
297
+ const rawManifest = yield driver.readManifest(output, entry, {
298
+ backupKey: key,
299
+ backupId: (0, BackupPathUtils_1.backupTimestampId)(),
300
+ log: () => null,
301
+ });
302
+ const manifest = normalizeManifest(rawManifest, entry);
303
+ if (!manifest || manifest.backupKey !== key) {
304
+ continue;
305
+ }
306
+ points.push(Object.assign(Object.assign({}, entry), { backupKey: manifest.backupKey, backupId: manifest.backupId, kind: manifest.kind || "normal", chainId: manifest.chainId, parentBackupId: manifest.parentBackupId, createdAt: new Date(manifest.createdAt || entry.createdAt), manifest: manifest }));
307
+ }
308
+ return points.sort((a, b) => restorePointCreatedAt(b).getTime() - restorePointCreatedAt(a).getTime());
309
+ });
310
+ }
311
+ listRestorePoints(key, options) {
312
+ return __awaiter(this, void 0, void 0, function* () {
313
+ const backupType = this.resolveType(key);
314
+ const output = this.chooseOutputForRead(backupType, options === null || options === void 0 ? void 0 : options.channels);
315
+ if (!output) {
316
+ throw new Error(`Backup type '${key}' has no matching output channels.`);
317
+ }
318
+ return this.listRestorePointsForOutput(key, output);
319
+ });
320
+ }
321
+ isFullIncrementalRun(backupType, previousPoint, options) {
322
+ var _a;
323
+ if (options === null || options === void 0 ? void 0 : options.full) {
324
+ return true;
325
+ }
326
+ if (!previousPoint) {
327
+ return true;
328
+ }
329
+ return (0, AutomationSchedule_1.matchesAutomationExecutions)((_a = backupType.incremental) === null || _a === void 0 ? void 0 : _a.fullExecutions, options === null || options === void 0 ? void 0 : options.scheduledAt);
330
+ }
331
+ buildBackupManifest(key, id, createdAt, artifactFilename, backupType, fileEntries, options) {
332
+ var _a, _b, _c;
333
+ return __awaiter(this, void 0, void 0, function* () {
334
+ const currentIndex = this.buildFileIndex(fileEntries, ((_a = backupType.incremental) === null || _a === void 0 ? void 0 : _a.checksum) === true);
335
+ const currentByTarget = new Map(fileEntries.map((entry) => [entry.target, entry]));
336
+ if (!this.incrementalEnabled(backupType)) {
337
+ return {
338
+ manifest: {
339
+ backupKey: key,
340
+ backupId: id,
341
+ kind: "normal",
342
+ createdAt: createdAt.toISOString(),
343
+ artifact: artifactFilename,
344
+ artifactType: backupType.zip ? "zip" : "directory",
345
+ files: {
346
+ upserted: fileEntries.map((entry) => (Object.assign(Object.assign({}, currentIndex[entry.target]), { source: entry.source }))),
347
+ deleted: [],
348
+ fileIndex: currentIndex,
349
+ },
350
+ },
351
+ filesToCopy: fileEntries,
352
+ previousPoint: null,
353
+ };
354
+ }
355
+ const stateOutput = this.chooseOutputForRead(backupType, options === null || options === void 0 ? void 0 : options.channels);
356
+ const restorePoints = stateOutput ? yield this.listRestorePointsForOutput(key, stateOutput) : [];
357
+ const previousPoint = restorePoints.find((point) => point.kind === "full" || point.kind === "incremental") || null;
358
+ const full = this.isFullIncrementalRun(backupType, previousPoint, options);
359
+ const previousIndex = ((_c = (_b = previousPoint === null || previousPoint === void 0 ? void 0 : previousPoint.manifest) === null || _b === void 0 ? void 0 : _b.files) === null || _c === void 0 ? void 0 : _c.fileIndex) || {};
360
+ const upserted = Object.values(currentIndex)
361
+ .filter((entry) => { var _a; return full || this.fileIndexChanged(entry, previousIndex[entry.path], ((_a = backupType.incremental) === null || _a === void 0 ? void 0 : _a.checksum) === true); })
362
+ .map((entry) => { var _a; return (Object.assign(Object.assign({}, entry), { source: (_a = currentByTarget.get(entry.path)) === null || _a === void 0 ? void 0 : _a.source })); });
363
+ const deleted = full
364
+ ? []
365
+ : Object.keys(previousIndex)
366
+ .filter((entryPath) => !currentIndex[entryPath])
367
+ .sort();
368
+ const upsertedPaths = new Set(upserted.map((entry) => entry.path));
369
+ const filesToCopy = full ? fileEntries : fileEntries.filter((entry) => upsertedPaths.has(entry.target));
370
+ const chainId = full ? id : (previousPoint === null || previousPoint === void 0 ? void 0 : previousPoint.chainId) || (previousPoint === null || previousPoint === void 0 ? void 0 : previousPoint.backupId) || id;
371
+ return {
372
+ manifest: {
373
+ backupKey: key,
374
+ backupId: id,
375
+ kind: full ? "full" : "incremental",
376
+ chainId: chainId,
377
+ parentBackupId: full ? undefined : previousPoint === null || previousPoint === void 0 ? void 0 : previousPoint.backupId,
378
+ createdAt: createdAt.toISOString(),
379
+ artifact: artifactFilename,
380
+ artifactType: "zip",
381
+ files: {
382
+ upserted: upserted,
383
+ deleted: deleted,
384
+ fileIndex: currentIndex,
385
+ },
386
+ },
387
+ filesToCopy: filesToCopy,
388
+ previousPoint: previousPoint,
389
+ };
390
+ });
391
+ }
392
+ writeContentManifest(contentDir, manifest) {
393
+ const manifestPath = path_1.default.join(contentDir, ".webframez-backup", "manifest.json");
394
+ fs_1.default.mkdirSync(path_1.default.dirname(manifestPath), { recursive: true });
395
+ fs_1.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 4), "utf-8");
396
+ }
196
397
  backupDatabase(source, contentDir, context) {
197
398
  return __awaiter(this, void 0, void 0, function* () {
198
399
  const to = (0, BackupPathUtils_1.normalizeBackupPath)(source.to || `database/${source.connection || "default"}`);
@@ -252,9 +453,12 @@ class BackupManager {
252
453
  return __awaiter(this, void 0, void 0, function* () {
253
454
  const backupType = this.resolveType(key);
254
455
  this.ensureActive(key, backupType);
456
+ this.assertIncrementalSupported(key, backupType);
255
457
  this.log(options, `Starting backup '${key}'`);
256
458
  const now = new Date();
257
459
  const id = (0, BackupPathUtils_1.backupTimestampId)(now);
460
+ const filename = (0, BackupPathUtils_1.formatBackupFilename)(backupType.filename, { key, date: now, id });
461
+ const artifactFilename = backupType.zip ? `${filename}.zip` : filename;
258
462
  const workRoot = (0, BackupPathUtils_1.resolveProjectPath)(backupType.workDir);
259
463
  const workRunDir = path_1.default.join(workRoot, `${key}_${id}`);
260
464
  const contentDir = path_1.default.join(workRunDir, "content");
@@ -277,11 +481,23 @@ class BackupManager {
277
481
  const fileEntries = normalizeArray(backupType.files).flatMap((source) => this.collectFilesFromSource(source, options));
278
482
  const fileSize = fileEntries.reduce((sum, entry) => sum + entry.size, 0);
279
483
  this.log(options, `Collected ${fileEntries.length} file(s)`, { size: fileSize });
280
- result.files = fileEntries.map((entry) => ({
484
+ const incremental = yield this.buildBackupManifest(key, id, now, artifactFilename, backupType, fileEntries, options);
485
+ result.kind = incremental.manifest.kind;
486
+ result.chainId = incremental.manifest.chainId;
487
+ result.parentBackupId = incremental.manifest.parentBackupId;
488
+ result.backupManifest = incremental.manifest;
489
+ result.files = incremental.filesToCopy.map((entry) => ({
281
490
  from: entry.source,
282
491
  to: entry.target,
283
492
  size: entry.size,
284
493
  }));
494
+ if (this.incrementalEnabled(backupType)) {
495
+ this.log(options, `Prepared ${incremental.manifest.kind} backup`, {
496
+ upserted: incremental.manifest.files.upserted.length,
497
+ deleted: incremental.manifest.files.deleted.length,
498
+ parentBackupId: incremental.manifest.parentBackupId,
499
+ });
500
+ }
285
501
  if (options === null || options === void 0 ? void 0 : options.dryRun) {
286
502
  this.log(options, "Dry run enabled; skipping artifact and output writes");
287
503
  result.outputs = outputs.map((output) => ({
@@ -300,8 +516,9 @@ class BackupManager {
300
516
  fs_1.default.rmSync(workRunDir, { recursive: true, force: true });
301
517
  fs_1.default.mkdirSync(contentDir, { recursive: true });
302
518
  try {
303
- this.log(options, `Copying ${fileEntries.length} file(s) into backup work directory`);
304
- this.copyFiles(fileEntries, contentDir, options);
519
+ this.log(options, `Copying ${incremental.filesToCopy.length} file(s) into backup work directory`);
520
+ this.copyFiles(incremental.filesToCopy, contentDir, options);
521
+ this.writeContentManifest(contentDir, incremental.manifest);
305
522
  this.log(options, "File copy finished");
306
523
  for (const source of normalizeArray(backupType.databases)) {
307
524
  this.log(options, `Backing up database '${source.connection || "default"}'`);
@@ -313,6 +530,7 @@ class BackupManager {
313
530
  this.log(options, `Database '${source.connection || "default"}' backup finished`);
314
531
  }
315
532
  result.artifact = this.buildArtifact(key, backupType, workRunDir, contentDir, now, id, options);
533
+ result.artifact.manifest = incremental.manifest;
316
534
  const manifestPath = path_1.default.join(workRunDir, `${result.artifact.filename}.run-manifest.json`);
317
535
  result.manifestPath = manifestPath;
318
536
  for (const output of outputs) {
@@ -369,6 +587,114 @@ class BackupManager {
369
587
  }
370
588
  });
371
589
  }
590
+ downloadRestoreArtifact(output, point, targetPath, key) {
591
+ return __awaiter(this, void 0, void 0, function* () {
592
+ const driver = BackupOutputDrivers_1.BackupOutputDrivers.get(output.driver);
593
+ if (!driver.downloadArtifact) {
594
+ throw new Error(`Backup output driver '${output.driver}' does not support restore downloads.`);
595
+ }
596
+ return driver.downloadArtifact(output, point, targetPath, {
597
+ backupKey: key,
598
+ backupId: point.backupId,
599
+ log: () => null,
600
+ });
601
+ });
602
+ }
603
+ restoreChain(points, target) {
604
+ if (target.kind === "normal") {
605
+ return [target];
606
+ }
607
+ const chainId = target.chainId || target.backupId;
608
+ const targetCreatedAt = restorePointCreatedAt(target).getTime();
609
+ const chain = points
610
+ .filter((point) => (point.chainId || point.backupId) === chainId)
611
+ .filter((point) => restorePointCreatedAt(point).getTime() <= targetCreatedAt)
612
+ .sort((a, b) => restorePointCreatedAt(a).getTime() - restorePointCreatedAt(b).getTime());
613
+ if (chain.length < 1 || chain[0].kind !== "full") {
614
+ throw new Error(`Could not find base full backup for incremental backup '${target.backupId}'.`);
615
+ }
616
+ if (!chain.find((point) => point.backupId === target.backupId)) {
617
+ throw new Error(`Could not build restore chain for backup '${target.backupId}'.`);
618
+ }
619
+ return chain;
620
+ }
621
+ extractArtifact(artifactPath, targetDir) {
622
+ return __awaiter(this, void 0, void 0, function* () {
623
+ yield (0, extract_zip_1.default)(artifactPath, { dir: path_1.default.resolve(targetDir) });
624
+ fs_1.default.rmSync(path_1.default.join(targetDir, ".webframez-backup"), { recursive: true, force: true });
625
+ });
626
+ }
627
+ restore(key, targetDir, options) {
628
+ var _a, _b, _c, _d;
629
+ return __awaiter(this, void 0, void 0, function* () {
630
+ const backupType = this.resolveType(key);
631
+ const output = this.chooseOutputForRead(backupType, options === null || options === void 0 ? void 0 : options.channels);
632
+ if (!output) {
633
+ throw new Error(`Backup type '${key}' has no matching output channels.`);
634
+ }
635
+ const driver = BackupOutputDrivers_1.BackupOutputDrivers.get(output.driver);
636
+ if (!driver.listArtifacts || !driver.readManifest || !driver.downloadArtifact) {
637
+ throw new Error(`Backup output driver '${output.driver}' does not support restore.`);
638
+ }
639
+ const restorePoints = yield this.listRestorePointsForOutput(key, output);
640
+ if (!(options === null || options === void 0 ? void 0 : options.backupId)) {
641
+ return {
642
+ backupKey: key,
643
+ driver: output.driver,
644
+ dryRun: options === null || options === void 0 ? void 0 : options.dryRun,
645
+ restorePoints: restorePoints,
646
+ };
647
+ }
648
+ if (!targetDir || targetDir.trim() === "") {
649
+ throw new Error("Restore target directory is required.");
650
+ }
651
+ const targetPoint = restorePoints.find((point) => point.backupId === options.backupId);
652
+ if (!targetPoint) {
653
+ throw new Error(`Backup '${options.backupId}' was not found for '${key}'.`);
654
+ }
655
+ const targetPath = path_1.default.resolve(targetDir);
656
+ if (fs_1.default.existsSync(targetPath) && fs_1.default.readdirSync(targetPath).length > 0) {
657
+ throw new Error(`Restore target directory must be new or empty: ${targetPath}`);
658
+ }
659
+ const chain = this.restoreChain(restorePoints, targetPoint);
660
+ const result = {
661
+ backupKey: key,
662
+ backupId: targetPoint.backupId,
663
+ targetDir: targetPath,
664
+ driver: output.driver,
665
+ dryRun: options === null || options === void 0 ? void 0 : options.dryRun,
666
+ chain: chain,
667
+ restored: [],
668
+ deleted: [],
669
+ };
670
+ if (options === null || options === void 0 ? void 0 : options.dryRun) {
671
+ return result;
672
+ }
673
+ fs_1.default.mkdirSync(targetPath, { recursive: true });
674
+ const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "webframez-backup-restore-"));
675
+ try {
676
+ for (const point of chain) {
677
+ const artifactPath = path_1.default.join(tempDir, point.filename);
678
+ yield this.downloadRestoreArtifact(output, point, artifactPath, key);
679
+ yield this.extractArtifact(artifactPath, targetPath);
680
+ (_a = result.restored) === null || _a === void 0 ? void 0 : _a.push({
681
+ artifact: point.filename,
682
+ backupId: point.backupId,
683
+ kind: point.kind,
684
+ });
685
+ for (const deletedPath of ((_c = (_b = point.manifest) === null || _b === void 0 ? void 0 : _b.files) === null || _c === void 0 ? void 0 : _c.deleted) || []) {
686
+ const deleteTarget = assertInside(targetPath, (0, BackupPathUtils_1.normalizeBackupPath)(deletedPath));
687
+ fs_1.default.rmSync(deleteTarget, { recursive: true, force: true });
688
+ (_d = result.deleted) === null || _d === void 0 ? void 0 : _d.push((0, BackupPathUtils_1.normalizeBackupPath)(deletedPath));
689
+ }
690
+ }
691
+ }
692
+ finally {
693
+ fs_1.default.rmSync(tempDir, { recursive: true, force: true });
694
+ }
695
+ return result;
696
+ });
697
+ }
372
698
  cleanup(key, options) {
373
699
  var _a;
374
700
  return __awaiter(this, void 0, void 0, function* () {
@@ -10,6 +10,11 @@ export type BackupFileSourceConfig = {
10
10
  exclude?: string[];
11
11
  optional?: boolean;
12
12
  };
13
+ export type BackupIncrementalConfig = {
14
+ enabled?: boolean;
15
+ fullExecutions?: any[];
16
+ checksum?: boolean;
17
+ };
13
18
  export type BackupDatabaseSourceConfig = {
14
19
  connection?: string;
15
20
  to?: string;
@@ -18,6 +23,7 @@ export type BackupDatabaseSourceConfig = {
18
23
  export type BackupOutputConfig = {
19
24
  driver: string;
20
25
  path?: string;
26
+ groupByDate?: boolean;
21
27
  retention?: BackupRetentionConfig;
22
28
  [key: string]: any;
23
29
  };
@@ -36,6 +42,7 @@ export type BackupDefaultsConfig = {
36
42
  zipCompressionLevel?: number;
37
43
  cleanupWorkDir?: boolean;
38
44
  retention?: BackupRetentionConfig;
45
+ incremental?: BackupIncrementalConfig;
39
46
  };
40
47
  export type BackupTypeConfig = BackupDefaultsConfig & {
41
48
  is_active?: boolean;
@@ -55,6 +62,7 @@ export type BackupArtifact = {
55
62
  filename: string;
56
63
  size: number;
57
64
  type: "zip" | "directory" | "manifest";
65
+ manifest?: BackupRunManifest;
58
66
  };
59
67
  export type BackupOutputResult = {
60
68
  driver: string;
@@ -69,6 +77,56 @@ export type BackupCleanupEntry = {
69
77
  createdAt: Date;
70
78
  size: number;
71
79
  reason: string;
80
+ payload?: any;
81
+ };
82
+ export type BackupFileIndexEntry = {
83
+ path: string;
84
+ size: number;
85
+ mtimeMs?: number;
86
+ checksum?: string;
87
+ };
88
+ export type BackupRunManifestFileChange = BackupFileIndexEntry & {
89
+ source?: string;
90
+ };
91
+ export type BackupRunManifest = {
92
+ backupKey: string;
93
+ backupId: string;
94
+ kind: "normal" | "full" | "incremental";
95
+ chainId?: string;
96
+ parentBackupId?: string;
97
+ createdAt: string;
98
+ artifact: string;
99
+ artifactType: "zip" | "directory" | "manifest";
100
+ files: {
101
+ upserted: BackupRunManifestFileChange[];
102
+ deleted: string[];
103
+ fileIndex: {
104
+ [path: string]: BackupFileIndexEntry;
105
+ };
106
+ };
107
+ };
108
+ export type BackupRestorePoint = BackupCleanupEntry & {
109
+ backupKey: string;
110
+ backupId: string;
111
+ kind: "normal" | "full" | "incremental";
112
+ chainId?: string;
113
+ parentBackupId?: string;
114
+ manifest?: BackupRunManifest | any;
115
+ };
116
+ export type BackupRestoreResult = {
117
+ backupKey: string;
118
+ backupId?: string;
119
+ targetDir?: string;
120
+ dryRun?: boolean;
121
+ driver?: string;
122
+ restorePoints?: BackupRestorePoint[];
123
+ chain?: BackupRestorePoint[];
124
+ restored?: Array<{
125
+ artifact: string;
126
+ backupId: string;
127
+ kind: string;
128
+ }>;
129
+ deleted?: string[];
72
130
  };
73
131
  export type BackupCleanupResult = {
74
132
  driver: string;
@@ -81,6 +139,9 @@ export type BackupCleanupResult = {
81
139
  export type BackupRunResult = {
82
140
  key: string;
83
141
  id: string;
142
+ kind?: "normal" | "full" | "incremental";
143
+ chainId?: string;
144
+ parentBackupId?: string;
84
145
  startedAt: string;
85
146
  endedAt: string;
86
147
  dryRun: boolean;
@@ -98,9 +159,12 @@ export type BackupRunResult = {
98
159
  to: string;
99
160
  payload?: any;
100
161
  }>;
162
+ backupManifest?: BackupRunManifest;
101
163
  };
102
164
  export type BackupRunOptions = {
103
165
  dryRun?: boolean;
166
+ full?: boolean;
167
+ scheduledAt?: Date | string;
104
168
  channels?: string[];
105
169
  silent?: boolean;
106
170
  logInterval?: number;
@@ -14,6 +14,7 @@ const BaseQueueJob_1 = require("../../Queue/BaseQueueJob");
14
14
  const BackupManager_1 = require("../BackupManager");
15
15
  class BackupRunJob extends BaseQueueJob_1.BaseQueueJob {
16
16
  handle(job) {
17
+ var _a;
17
18
  return __awaiter(this, void 0, void 0, function* () {
18
19
  const backupKey = job && job.payload ? job.payload.backupKey : null;
19
20
  if (!backupKey || typeof backupKey !== "string") {
@@ -22,6 +23,8 @@ class BackupRunJob extends BaseQueueJob_1.BaseQueueJob {
22
23
  this.log(`Starting backup '${backupKey}'`);
23
24
  const result = yield new BackupManager_1.BackupManager().run(backupKey, {
24
25
  channels: job.payload.channels,
26
+ full: job.payload.full === true,
27
+ scheduledAt: (_a = job.automation_execution) === null || _a === void 0 ? void 0 : _a.scheduled_at,
25
28
  log: (message, payload) => this.log(message, payload),
26
29
  });
27
30
  this.log(`Backup '${backupKey}' finished`, {
@@ -1,4 +1,4 @@
1
- import { BackupArtifact, BackupCleanupOptions, BackupCleanupResult, BackupOutputConfig, BackupOutputResult, BackupRetentionConfig } from "../BackupTypes";
1
+ import { BackupArtifact, BackupCleanupOptions, BackupCleanupResult, BackupCleanupEntry, BackupOutputConfig, BackupOutputResult, BackupRetentionConfig } from "../BackupTypes";
2
2
  export type BackupOutputDriverContext = {
3
3
  backupKey: string;
4
4
  backupId: string;
@@ -8,4 +8,7 @@ export type BackupOutputDriverContext = {
8
8
  export interface BaseBackupOutputDriver {
9
9
  write(artifact: BackupArtifact, output: BackupOutputConfig, context: BackupOutputDriverContext): Promise<BackupOutputResult>;
10
10
  cleanup(output: BackupOutputConfig, context: BackupOutputDriverContext, options?: BackupCleanupOptions): Promise<BackupCleanupResult>;
11
+ listArtifacts?(output: BackupOutputConfig, context: BackupOutputDriverContext): Promise<BackupCleanupEntry[]> | BackupCleanupEntry[];
12
+ readManifest?(output: BackupOutputConfig, entry: BackupCleanupEntry, context: BackupOutputDriverContext): Promise<any> | any;
13
+ downloadArtifact?(output: BackupOutputConfig, entry: BackupCleanupEntry, targetPath: string, context: BackupOutputDriverContext): Promise<string> | string;
11
14
  }
@@ -2,11 +2,18 @@ import { BackupArtifact, BackupCleanupEntry, BackupCleanupOptions, BackupCleanup
2
2
  import { BaseBackupOutputDriver, BackupOutputDriverContext } from "./BaseBackupOutputDriver";
3
3
  type LocalBackupEntry = BackupCleanupEntry & {
4
4
  manifestPath?: string;
5
+ payload?: any;
5
6
  };
6
7
  export declare class LocalBackupOutputDriver implements BaseBackupOutputDriver {
7
8
  private outputPath;
9
+ private targetDir;
10
+ private listDirs;
8
11
  write(artifact: BackupArtifact, output: BackupOutputConfig, context: BackupOutputDriverContext): Promise<BackupOutputResult>;
12
+ listArtifacts(output: BackupOutputConfig, context: BackupOutputDriverContext): LocalBackupEntry[];
9
13
  list(output: BackupOutputConfig, context: BackupOutputDriverContext): LocalBackupEntry[];
14
+ readManifest(output: BackupOutputConfig, entry: LocalBackupEntry): any;
15
+ downloadArtifact(output: BackupOutputConfig, entry: LocalBackupEntry, targetPath: string): string;
16
+ private protectIncrementalChains;
10
17
  cleanup(output: BackupOutputConfig, context: BackupOutputDriverContext, options?: BackupCleanupOptions): Promise<BackupCleanupResult>;
11
18
  }
12
19
  export {};