@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.
- package/dist/Backup/BackupManager.d.ts +21 -1
- package/dist/Backup/BackupManager.js +330 -4
- package/dist/Backup/BackupTypes.d.ts +64 -0
- package/dist/Backup/Jobs/BackupRunJob.js +3 -0
- package/dist/Backup/OutputDrivers/BaseBackupOutputDriver.d.ts +4 -1
- package/dist/Backup/OutputDrivers/LocalBackupOutputDriver.d.ts +7 -0
- package/dist/Backup/OutputDrivers/LocalBackupOutputDriver.js +125 -45
- package/dist/Commands/BackupRestoreCommand.d.ts +6 -0
- package/dist/Commands/BackupRestoreCommand.js +62 -0
- package/dist/Commands/BackupRestorePointsCommand.d.ts +6 -0
- package/dist/Commands/BackupRestorePointsCommand.js +48 -0
- package/dist/Commands/BackupRunCommand.js +5 -0
- package/dist/Commands/QueueWorkerCommand.d.ts +0 -1
- package/dist/Commands/QueueWorkerCommand.js +8 -93
- package/dist/ConsoleApplication.js +4 -0
- package/dist/Queue/AutomationSchedule.d.ts +10 -0
- package/dist/Queue/AutomationSchedule.js +126 -0
- package/dist/Queue/BaseQueueJob.js +7 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +2 -1
|
@@ -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:
|
|
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
|
-
|
|
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 ${
|
|
304
|
-
this.copyFiles(
|
|
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 {};
|