@camstack/system 1.2.19 → 1.2.21
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.
|
@@ -3,6 +3,15 @@ export declare class BackupOrchestratorAddon extends BaseAddon<Record<string, ne
|
|
|
3
3
|
private systemBackup;
|
|
4
4
|
private policies;
|
|
5
5
|
private dataDir;
|
|
6
|
+
/**
|
|
7
|
+
* Transient build buffer for archive fan-out. The tar is built here
|
|
8
|
+
* ONCE, streamed to every destination via the storage cap's chunked
|
|
9
|
+
* upload, then removed. Lives in the addon's private `ctx.dataDir`
|
|
10
|
+
* (never `api.storage.resolve`, per the AddonContext contract) — a
|
|
11
|
+
* `cache` storage location is never declared, so resolving one threw
|
|
12
|
+
* and aborted every run before a byte was written.
|
|
13
|
+
*/
|
|
14
|
+
private stagingDir;
|
|
6
15
|
private scheduleTimer;
|
|
7
16
|
/**
|
|
8
17
|
* Baseline used as the "after" anchor when a policy has a cron but
|
|
@@ -47,12 +56,13 @@ export declare class BackupOrchestratorAddon extends BaseAddon<Record<string, ne
|
|
|
47
56
|
* unreachable destination doesn't blank out the whole UI.
|
|
48
57
|
*/
|
|
49
58
|
private aggregatedList;
|
|
59
|
+
private stagingPath;
|
|
50
60
|
/**
|
|
51
|
-
* Build a single archive in the
|
|
61
|
+
* Build a single archive in the private staging dir, then push
|
|
52
62
|
* it to every selected destination via the storage cap's chunked
|
|
53
63
|
* upload protocol. Per-destination errors are logged and counted
|
|
54
64
|
* via allSettled semantics — one bad sink doesn't kill the run. The
|
|
55
|
-
*
|
|
65
|
+
* staging file is deleted in `finally` so a crash mid-fanout doesn't
|
|
56
66
|
* leak.
|
|
57
67
|
*/
|
|
58
68
|
private triggerBackup;
|
|
@@ -99,8 +109,8 @@ export declare class BackupOrchestratorAddon extends BaseAddon<Record<string, ne
|
|
|
99
109
|
/**
|
|
100
110
|
* Schedule a restore for the next boot. Pulls the archive bytes
|
|
101
111
|
* down from the destination's storage location via the chunked-
|
|
102
|
-
* download protocol into the
|
|
103
|
-
* usual restore marker against the
|
|
112
|
+
* download protocol into the private staging dir, then writes the
|
|
113
|
+
* usual restore marker against the staged path so the boot-time
|
|
104
114
|
* apply hook (in server-backend launcher) extracts before any
|
|
105
115
|
* addon starts.
|
|
106
116
|
*
|
|
@@ -3130,13 +3130,20 @@ var SystemBackupService = class {
|
|
|
3130
3130
|
try {
|
|
3131
3131
|
const stagedInDataDir = node_path.join(this.dataDir, ARCHIVE_MANIFEST_NAME);
|
|
3132
3132
|
node_fs.copyFileSync(manifestStagePath, stagedInDataDir);
|
|
3133
|
+
const stagingRel = node_path.relative(this.dataDir, node_path.dirname(opts.archivePath));
|
|
3134
|
+
const excludePrefix = stagingRel && !stagingRel.startsWith("..") && !node_path.isAbsolute(stagingRel) ? stagingRel : null;
|
|
3133
3135
|
try {
|
|
3134
3136
|
await Kn({
|
|
3135
3137
|
file: opts.archivePath,
|
|
3136
3138
|
gzip: true,
|
|
3137
3139
|
cwd: this.dataDir,
|
|
3138
3140
|
follow: true,
|
|
3139
|
-
portable: true
|
|
3141
|
+
portable: true,
|
|
3142
|
+
filter: (entryPath) => {
|
|
3143
|
+
if (!excludePrefix) return true;
|
|
3144
|
+
const norm = entryPath.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
3145
|
+
return norm !== excludePrefix && !norm.startsWith(`${excludePrefix}/`);
|
|
3146
|
+
}
|
|
3140
3147
|
}, [ARCHIVE_MANIFEST_NAME, ...present]);
|
|
3141
3148
|
} finally {
|
|
3142
3149
|
try {
|
|
@@ -4587,8 +4594,8 @@ function previewNextRuns(cron, count, after) {
|
|
|
4587
4594
|
* that list with the `backup_destination_policies` table to surface
|
|
4588
4595
|
* `enabled` / `retentionCount` / `label`. The actual archive bytes
|
|
4589
4596
|
* travel through `api.storage.beginUpload` / `writeChunk` /
|
|
4590
|
-
* `finalizeUpload`, with the staging archive built into the
|
|
4591
|
-
*
|
|
4597
|
+
* `finalizeUpload`, with the staging archive built into the addon's
|
|
4598
|
+
* private `ctx.dataDir/staging` dir.
|
|
4592
4599
|
*/
|
|
4593
4600
|
/**
|
|
4594
4601
|
* Hardcoded seed for newly-discovered `backups` locations. Operators
|
|
@@ -4636,6 +4643,15 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4636
4643
|
systemBackup = null;
|
|
4637
4644
|
policies = null;
|
|
4638
4645
|
dataDir = "";
|
|
4646
|
+
/**
|
|
4647
|
+
* Transient build buffer for archive fan-out. The tar is built here
|
|
4648
|
+
* ONCE, streamed to every destination via the storage cap's chunked
|
|
4649
|
+
* upload, then removed. Lives in the addon's private `ctx.dataDir`
|
|
4650
|
+
* (never `api.storage.resolve`, per the AddonContext contract) — a
|
|
4651
|
+
* `cache` storage location is never declared, so resolving one threw
|
|
4652
|
+
* and aborted every run before a byte was written.
|
|
4653
|
+
*/
|
|
4654
|
+
stagingDir = "";
|
|
4639
4655
|
scheduleTimer = null;
|
|
4640
4656
|
/**
|
|
4641
4657
|
* Baseline used as the "after" anchor when a policy has a cron but
|
|
@@ -4650,6 +4666,7 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4650
4666
|
}
|
|
4651
4667
|
async onInitialize() {
|
|
4652
4668
|
this.dataDir = this.ctx.nodeDataDir;
|
|
4669
|
+
this.stagingDir = node_path.join(this.ctx.dataDir, "staging");
|
|
4653
4670
|
this.systemBackup = new SystemBackupService(this.dataDir, this.ctx.logger);
|
|
4654
4671
|
this.policies = this.resolvePolicyService();
|
|
4655
4672
|
if (this.policies) try {
|
|
@@ -4826,12 +4843,16 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4826
4843
|
}
|
|
4827
4844
|
return out;
|
|
4828
4845
|
}
|
|
4846
|
+
async stagingPath(filename) {
|
|
4847
|
+
await node_fs_promises.mkdir(this.stagingDir, { recursive: true });
|
|
4848
|
+
return node_path.join(this.stagingDir, filename);
|
|
4849
|
+
}
|
|
4829
4850
|
/**
|
|
4830
|
-
* Build a single archive in the
|
|
4851
|
+
* Build a single archive in the private staging dir, then push
|
|
4831
4852
|
* it to every selected destination via the storage cap's chunked
|
|
4832
4853
|
* upload protocol. Per-destination errors are logged and counted
|
|
4833
4854
|
* via allSettled semantics — one bad sink doesn't kill the run. The
|
|
4834
|
-
*
|
|
4855
|
+
* staging file is deleted in `finally` so a crash mid-fanout doesn't
|
|
4835
4856
|
* leak.
|
|
4836
4857
|
*/
|
|
4837
4858
|
async triggerBackup(input) {
|
|
@@ -4844,11 +4865,7 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
4844
4865
|
const policies = await this.loadPoliciesIndexed(targets.map((t) => t.id));
|
|
4845
4866
|
const archiveId = (0, node_crypto.randomUUID)();
|
|
4846
4867
|
const archiveFilename = `${archiveId}.tar.gz`;
|
|
4847
|
-
const
|
|
4848
|
-
const cachePath = await this.ctx.api.storage.resolve.query({
|
|
4849
|
-
location: "cache",
|
|
4850
|
-
relativePath: cacheRelativePath
|
|
4851
|
-
});
|
|
4868
|
+
const cachePath = await this.stagingPath(archiveFilename);
|
|
4852
4869
|
const result = await this.systemBackup.createArchive({
|
|
4853
4870
|
archivePath: cachePath,
|
|
4854
4871
|
...input?.locations ? { locations: input.locations } : {}
|
|
@@ -5092,8 +5109,8 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5092
5109
|
/**
|
|
5093
5110
|
* Schedule a restore for the next boot. Pulls the archive bytes
|
|
5094
5111
|
* down from the destination's storage location via the chunked-
|
|
5095
|
-
* download protocol into the
|
|
5096
|
-
* usual restore marker against the
|
|
5112
|
+
* download protocol into the private staging dir, then writes the
|
|
5113
|
+
* usual restore marker against the staged path so the boot-time
|
|
5097
5114
|
* apply hook (in server-backend launcher) extracts before any
|
|
5098
5115
|
* addon starts.
|
|
5099
5116
|
*
|
|
@@ -5104,10 +5121,7 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5104
5121
|
if (!this.systemBackup) throw new Error("orchestrator not initialized");
|
|
5105
5122
|
const entry = (await readLocationManifest(this.storageApi(), input.destinationId)).archives.find((a) => a.id === input.backupId);
|
|
5106
5123
|
if (!entry) throw new Error(`backup restore: backup "${input.backupId}" not found at location "${input.destinationId}"`);
|
|
5107
|
-
const cachePath = await this.
|
|
5108
|
-
location: "cache",
|
|
5109
|
-
relativePath: `restore-${input.backupId}.tar.gz`
|
|
5110
|
-
});
|
|
5124
|
+
const cachePath = await this.stagingPath(`restore-${input.backupId}.tar.gz`);
|
|
5111
5125
|
await downloadArchiveToCache({
|
|
5112
5126
|
api: this.ctx.api.storage,
|
|
5113
5127
|
locationId: input.destinationId,
|
|
@@ -3115,13 +3115,20 @@ var SystemBackupService = class {
|
|
|
3115
3115
|
try {
|
|
3116
3116
|
const stagedInDataDir = path$1.join(this.dataDir, ARCHIVE_MANIFEST_NAME);
|
|
3117
3117
|
fs.copyFileSync(manifestStagePath, stagedInDataDir);
|
|
3118
|
+
const stagingRel = path$1.relative(this.dataDir, path$1.dirname(opts.archivePath));
|
|
3119
|
+
const excludePrefix = stagingRel && !stagingRel.startsWith("..") && !path$1.isAbsolute(stagingRel) ? stagingRel : null;
|
|
3118
3120
|
try {
|
|
3119
3121
|
await Kn({
|
|
3120
3122
|
file: opts.archivePath,
|
|
3121
3123
|
gzip: true,
|
|
3122
3124
|
cwd: this.dataDir,
|
|
3123
3125
|
follow: true,
|
|
3124
|
-
portable: true
|
|
3126
|
+
portable: true,
|
|
3127
|
+
filter: (entryPath) => {
|
|
3128
|
+
if (!excludePrefix) return true;
|
|
3129
|
+
const norm = entryPath.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
3130
|
+
return norm !== excludePrefix && !norm.startsWith(`${excludePrefix}/`);
|
|
3131
|
+
}
|
|
3125
3132
|
}, [ARCHIVE_MANIFEST_NAME, ...present]);
|
|
3126
3133
|
} finally {
|
|
3127
3134
|
try {
|
|
@@ -4572,8 +4579,8 @@ function previewNextRuns(cron, count, after) {
|
|
|
4572
4579
|
* that list with the `backup_destination_policies` table to surface
|
|
4573
4580
|
* `enabled` / `retentionCount` / `label`. The actual archive bytes
|
|
4574
4581
|
* travel through `api.storage.beginUpload` / `writeChunk` /
|
|
4575
|
-
* `finalizeUpload`, with the staging archive built into the
|
|
4576
|
-
*
|
|
4582
|
+
* `finalizeUpload`, with the staging archive built into the addon's
|
|
4583
|
+
* private `ctx.dataDir/staging` dir.
|
|
4577
4584
|
*/
|
|
4578
4585
|
/**
|
|
4579
4586
|
* Hardcoded seed for newly-discovered `backups` locations. Operators
|
|
@@ -4621,6 +4628,15 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
4621
4628
|
systemBackup = null;
|
|
4622
4629
|
policies = null;
|
|
4623
4630
|
dataDir = "";
|
|
4631
|
+
/**
|
|
4632
|
+
* Transient build buffer for archive fan-out. The tar is built here
|
|
4633
|
+
* ONCE, streamed to every destination via the storage cap's chunked
|
|
4634
|
+
* upload, then removed. Lives in the addon's private `ctx.dataDir`
|
|
4635
|
+
* (never `api.storage.resolve`, per the AddonContext contract) — a
|
|
4636
|
+
* `cache` storage location is never declared, so resolving one threw
|
|
4637
|
+
* and aborted every run before a byte was written.
|
|
4638
|
+
*/
|
|
4639
|
+
stagingDir = "";
|
|
4624
4640
|
scheduleTimer = null;
|
|
4625
4641
|
/**
|
|
4626
4642
|
* Baseline used as the "after" anchor when a policy has a cron but
|
|
@@ -4635,6 +4651,7 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
4635
4651
|
}
|
|
4636
4652
|
async onInitialize() {
|
|
4637
4653
|
this.dataDir = this.ctx.nodeDataDir;
|
|
4654
|
+
this.stagingDir = path$1.join(this.ctx.dataDir, "staging");
|
|
4638
4655
|
this.systemBackup = new SystemBackupService(this.dataDir, this.ctx.logger);
|
|
4639
4656
|
this.policies = this.resolvePolicyService();
|
|
4640
4657
|
if (this.policies) try {
|
|
@@ -4811,12 +4828,16 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
4811
4828
|
}
|
|
4812
4829
|
return out;
|
|
4813
4830
|
}
|
|
4831
|
+
async stagingPath(filename) {
|
|
4832
|
+
await fsp.mkdir(this.stagingDir, { recursive: true });
|
|
4833
|
+
return path$1.join(this.stagingDir, filename);
|
|
4834
|
+
}
|
|
4814
4835
|
/**
|
|
4815
|
-
* Build a single archive in the
|
|
4836
|
+
* Build a single archive in the private staging dir, then push
|
|
4816
4837
|
* it to every selected destination via the storage cap's chunked
|
|
4817
4838
|
* upload protocol. Per-destination errors are logged and counted
|
|
4818
4839
|
* via allSettled semantics — one bad sink doesn't kill the run. The
|
|
4819
|
-
*
|
|
4840
|
+
* staging file is deleted in `finally` so a crash mid-fanout doesn't
|
|
4820
4841
|
* leak.
|
|
4821
4842
|
*/
|
|
4822
4843
|
async triggerBackup(input) {
|
|
@@ -4829,11 +4850,7 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
4829
4850
|
const policies = await this.loadPoliciesIndexed(targets.map((t) => t.id));
|
|
4830
4851
|
const archiveId = randomUUID();
|
|
4831
4852
|
const archiveFilename = `${archiveId}.tar.gz`;
|
|
4832
|
-
const
|
|
4833
|
-
const cachePath = await this.ctx.api.storage.resolve.query({
|
|
4834
|
-
location: "cache",
|
|
4835
|
-
relativePath: cacheRelativePath
|
|
4836
|
-
});
|
|
4853
|
+
const cachePath = await this.stagingPath(archiveFilename);
|
|
4837
4854
|
const result = await this.systemBackup.createArchive({
|
|
4838
4855
|
archivePath: cachePath,
|
|
4839
4856
|
...input?.locations ? { locations: input.locations } : {}
|
|
@@ -5077,8 +5094,8 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5077
5094
|
/**
|
|
5078
5095
|
* Schedule a restore for the next boot. Pulls the archive bytes
|
|
5079
5096
|
* down from the destination's storage location via the chunked-
|
|
5080
|
-
* download protocol into the
|
|
5081
|
-
* usual restore marker against the
|
|
5097
|
+
* download protocol into the private staging dir, then writes the
|
|
5098
|
+
* usual restore marker against the staged path so the boot-time
|
|
5082
5099
|
* apply hook (in server-backend launcher) extracts before any
|
|
5083
5100
|
* addon starts.
|
|
5084
5101
|
*
|
|
@@ -5089,10 +5106,7 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5089
5106
|
if (!this.systemBackup) throw new Error("orchestrator not initialized");
|
|
5090
5107
|
const entry = (await readLocationManifest(this.storageApi(), input.destinationId)).archives.find((a) => a.id === input.backupId);
|
|
5091
5108
|
if (!entry) throw new Error(`backup restore: backup "${input.backupId}" not found at location "${input.destinationId}"`);
|
|
5092
|
-
const cachePath = await this.
|
|
5093
|
-
location: "cache",
|
|
5094
|
-
relativePath: `restore-${input.backupId}.tar.gz`
|
|
5095
|
-
});
|
|
5109
|
+
const cachePath = await this.stagingPath(`restore-${input.backupId}.tar.gz`);
|
|
5096
5110
|
await downloadArchiveToCache({
|
|
5097
5111
|
api: this.ctx.api.storage,
|
|
5098
5112
|
locationId: input.destinationId,
|