@camstack/server 1.2.425 → 1.2.427
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/api/trpc/generated-cap-mounts.js +3 -1
- package/dist/api/trpc/generated-cap-routers.js +1724 -1700
- package/dist/launcher.js +2 -78
- package/dist/pending-restore.js +199 -0
- package/dist/sqlite-restore.js +150 -0
- package/package.json +6 -6
package/dist/launcher.js
CHANGED
|
@@ -55,7 +55,6 @@ const fs = __importStar(require("node:fs"));
|
|
|
55
55
|
const os = __importStar(require("node:os"));
|
|
56
56
|
const path = __importStar(require("node:path"));
|
|
57
57
|
const yaml = __importStar(require("js-yaml"));
|
|
58
|
-
const tar = __importStar(require("tar"));
|
|
59
58
|
const addon_root_inventory_js_1 = require("./addon-root-inventory.js");
|
|
60
59
|
const bootstrap_packages_js_1 = require("./bootstrap-packages.js");
|
|
61
60
|
const first_boot_addon_plan_js_1 = require("./first-boot-addon-plan.js");
|
|
@@ -63,84 +62,9 @@ const addon_realign_plan_js_1 = require("./core/addon/addon-realign-plan.js");
|
|
|
63
62
|
const closure_node_modules_js_1 = require("./closure-node-modules.js");
|
|
64
63
|
const framework_nodepath_js_1 = require("./framework-nodepath.js");
|
|
65
64
|
const package_inventory_js_1 = require("./package-inventory.js");
|
|
66
|
-
|
|
67
|
-
const ARCHIVE_MANIFEST_NAME = '.camstack-backup-manifest.json';
|
|
65
|
+
const pending_restore_js_1 = require("./pending-restore.js");
|
|
68
66
|
/** Resolve the data directory from env or default */
|
|
69
67
|
const DATA_DIR = process.env['CAMSTACK_DATA'] ?? 'camstack-data';
|
|
70
|
-
const RESTORE_MARKER_FILE = '.pending-restore.json';
|
|
71
|
-
/**
|
|
72
|
-
* Apply a pending system-restore archive over `dataDir` if a marker is
|
|
73
|
-
* present. Runs BEFORE the addon installer so the restored snapshot's
|
|
74
|
-
* `addons/` folder is what `ensureRequiredPackages` picks up.
|
|
75
|
-
*
|
|
76
|
-
* Marker contract is owned by `SystemBackupService.scheduleRestoreMarker`
|
|
77
|
-
* (packages/core/src/builtins/system-backup) — kept inline here so the
|
|
78
|
-
* launcher remains zero-core-deps. Schema:
|
|
79
|
-
* { archivePath: string, requestedAt: number, source: string }
|
|
80
|
-
*
|
|
81
|
-
* Failure modes:
|
|
82
|
-
* - marker malformed → log + leave it in place (operator visibility)
|
|
83
|
-
* - archive missing → log + clear marker (auto-recover from stale)
|
|
84
|
-
* - tar fails → throw → launcher exits non-zero, marker preserved
|
|
85
|
-
*/
|
|
86
|
-
async function applyPendingRestore(dataDir) {
|
|
87
|
-
const markerPath = path.join(dataDir, RESTORE_MARKER_FILE);
|
|
88
|
-
if (!fs.existsSync(markerPath))
|
|
89
|
-
return;
|
|
90
|
-
console.log(`[launcher] Pending restore marker found at ${markerPath}`);
|
|
91
|
-
let archivePath = null;
|
|
92
|
-
let locations = null;
|
|
93
|
-
try {
|
|
94
|
-
const raw = fs.readFileSync(markerPath, 'utf-8');
|
|
95
|
-
const parsed = JSON.parse(raw);
|
|
96
|
-
if (typeof parsed === 'object' &&
|
|
97
|
-
parsed != null &&
|
|
98
|
-
typeof parsed.archivePath === 'string') {
|
|
99
|
-
archivePath = parsed.archivePath;
|
|
100
|
-
}
|
|
101
|
-
const locField = parsed.locations;
|
|
102
|
-
if (Array.isArray(locField) && locField.every((l) => typeof l === 'string')) {
|
|
103
|
-
locations = locField;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
catch (err) {
|
|
107
|
-
console.error('[launcher] Restore marker malformed — leaving in place:', err);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (archivePath == null) {
|
|
111
|
-
console.error('[launcher] Restore marker missing archivePath — clearing');
|
|
112
|
-
fs.rmSync(markerPath, { force: true });
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (!fs.existsSync(archivePath)) {
|
|
116
|
-
console.error(`[launcher] Restore archive ${archivePath} missing — clearing marker`);
|
|
117
|
-
fs.rmSync(markerPath, { force: true });
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const scope = locations ? ` (only: ${locations.join(', ')})` : '';
|
|
121
|
-
console.log(`[launcher] Restoring snapshot from ${archivePath} into ${dataDir}${scope}`);
|
|
122
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
123
|
-
// Same filter logic SystemBackupService.extractArchive uses — kept
|
|
124
|
-
// inline here so the launcher stays zero-core-deps. The shape is
|
|
125
|
-
// straightforward enough to duplicate.
|
|
126
|
-
const allowedPrefixes = locations
|
|
127
|
-
? locations.map((loc) => loc.replace(/^\.\//, '').replace(/\/+$/, ''))
|
|
128
|
-
: null;
|
|
129
|
-
await tar.extract({
|
|
130
|
-
file: archivePath,
|
|
131
|
-
cwd: dataDir,
|
|
132
|
-
filter: (p) => {
|
|
133
|
-
if (p === ARCHIVE_MANIFEST_NAME || p === `./${ARCHIVE_MANIFEST_NAME}`)
|
|
134
|
-
return false;
|
|
135
|
-
if (!allowedPrefixes)
|
|
136
|
-
return true;
|
|
137
|
-
const normalized = p.replace(/^\.\//, '');
|
|
138
|
-
return allowedPrefixes.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`));
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
fs.rmSync(markerPath, { force: true });
|
|
142
|
-
console.log('[launcher] Restore complete; marker cleared');
|
|
143
|
-
}
|
|
144
68
|
/** Narrow `CAMSTACK_INSTALL_SOURCE` env into the typed union. */
|
|
145
69
|
function parseInstallSource(value) {
|
|
146
70
|
if (value === 'npm' || value === 'local' || value === 'symlink')
|
|
@@ -321,7 +245,7 @@ async function launch() {
|
|
|
321
245
|
// Restore must happen first so the snapshot's `addons/` is what the
|
|
322
246
|
// installer + symlink step pick up — otherwise we'd install a stale
|
|
323
247
|
// set and overwrite it a step later.
|
|
324
|
-
await applyPendingRestore(dataDir);
|
|
248
|
+
await (0, pending_restore_js_1.applyPendingRestore)(dataDir, (line) => console.log(line));
|
|
325
249
|
// @camstack/system is imported DYNAMICALLY here — AFTER the active framework
|
|
326
250
|
// dir + NODE_PATH are resolved above — so the correct framework copy is what
|
|
327
251
|
// gets loaded. Never import it at module top.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.applyPendingRestore = applyPendingRestore;
|
|
37
|
+
/**
|
|
38
|
+
* The boot-time system restore — the ONLY restore there is. The launcher calls
|
|
39
|
+
* it before the addon installer and before any runner starts, so nothing has a
|
|
40
|
+
* file of the data dir open.
|
|
41
|
+
*
|
|
42
|
+
* Marker contract is owned by `SystemBackupService.scheduleRestoreMarker`
|
|
43
|
+
* (`packages/system/src/builtins/system-backup`) — read inline here so the
|
|
44
|
+
* launcher stays free of `@camstack/*` runtime imports. Schema:
|
|
45
|
+
* { archivePath: string, requestedAt: number, source: string, locations?: string[] }
|
|
46
|
+
*
|
|
47
|
+
* After the extraction, SQLite databases get their own step
|
|
48
|
+
* (`sqlite-restore.ts`, D693): an owner snapshot is promoted over its database,
|
|
49
|
+
* and a sidecar the archive did not bring is dropped, because SQLite would
|
|
50
|
+
* replay a stale `-wal` onto the restored file.
|
|
51
|
+
*
|
|
52
|
+
* Failure modes:
|
|
53
|
+
* - marker malformed → log + leave it in place (operator visibility)
|
|
54
|
+
* - archive missing → log + clear marker (auto-recover from stale)
|
|
55
|
+
* - tar fails → throw → launcher exits non-zero, marker preserved
|
|
56
|
+
*/
|
|
57
|
+
const fs = __importStar(require("node:fs"));
|
|
58
|
+
const path = __importStar(require("node:path"));
|
|
59
|
+
const tar = __importStar(require("tar"));
|
|
60
|
+
const sqlite_restore_js_1 = require("./sqlite-restore.js");
|
|
61
|
+
/** Path of the manifest file embedded inside every archive. */
|
|
62
|
+
const ARCHIVE_MANIFEST_NAME = '.camstack-backup-manifest.json';
|
|
63
|
+
const RESTORE_MARKER_FILE = '.pending-restore.json';
|
|
64
|
+
/**
|
|
65
|
+
* Apply a pending system-restore archive over `dataDir` if a marker is
|
|
66
|
+
* present. Runs BEFORE the addon installer so the restored snapshot's
|
|
67
|
+
* `addons/` folder is what `ensureRequiredPackages` picks up.
|
|
68
|
+
*/
|
|
69
|
+
async function applyPendingRestore(dataDir, log) {
|
|
70
|
+
const markerPath = path.join(dataDir, RESTORE_MARKER_FILE);
|
|
71
|
+
if (!fs.existsSync(markerPath))
|
|
72
|
+
return;
|
|
73
|
+
log(`[launcher] Pending restore marker found at ${markerPath}`);
|
|
74
|
+
let archivePath = null;
|
|
75
|
+
let locations = null;
|
|
76
|
+
try {
|
|
77
|
+
const raw = fs.readFileSync(markerPath, 'utf-8');
|
|
78
|
+
const parsed = JSON.parse(raw);
|
|
79
|
+
if (typeof parsed === 'object' && parsed != null) {
|
|
80
|
+
if ('archivePath' in parsed && typeof parsed.archivePath === 'string') {
|
|
81
|
+
archivePath = parsed.archivePath;
|
|
82
|
+
}
|
|
83
|
+
if ('locations' in parsed &&
|
|
84
|
+
Array.isArray(parsed.locations) &&
|
|
85
|
+
parsed.locations.every((l) => typeof l === 'string')) {
|
|
86
|
+
locations = parsed.locations.filter((l) => typeof l === 'string');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
log(`[launcher] Restore marker malformed — leaving in place: ${String(err)}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (archivePath == null) {
|
|
95
|
+
log('[launcher] Restore marker missing archivePath — clearing');
|
|
96
|
+
fs.rmSync(markerPath, { force: true });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!fs.existsSync(archivePath)) {
|
|
100
|
+
log(`[launcher] Restore archive ${archivePath} missing — clearing marker`);
|
|
101
|
+
fs.rmSync(markerPath, { force: true });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const scope = locations ? ` (only: ${locations.join(', ')})` : '';
|
|
105
|
+
log(`[launcher] Restoring snapshot from ${archivePath} into ${dataDir}${scope}`);
|
|
106
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
107
|
+
const allowedPrefixes = locations
|
|
108
|
+
? locations.map((loc) => loc.replace(/^\.\//, '').replace(/\/+$/, ''))
|
|
109
|
+
: null;
|
|
110
|
+
const inScope = (normalized) => !allowedPrefixes ||
|
|
111
|
+
allowedPrefixes.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`));
|
|
112
|
+
// What the archive says it carries — read before the extraction, from the
|
|
113
|
+
// manifest `createArchive` writes as the archive's FIRST entry.
|
|
114
|
+
const promised = await readPromisedSnapshots(archivePath, log);
|
|
115
|
+
// Every path this extraction writes — the SQLite step decides from it alone.
|
|
116
|
+
const extracted = [];
|
|
117
|
+
await tar.extract({
|
|
118
|
+
file: archivePath,
|
|
119
|
+
cwd: dataDir,
|
|
120
|
+
filter: (p) => {
|
|
121
|
+
if (p === ARCHIVE_MANIFEST_NAME || p === `./${ARCHIVE_MANIFEST_NAME}`)
|
|
122
|
+
return false;
|
|
123
|
+
const normalized = p.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
124
|
+
const keep = inScope(normalized);
|
|
125
|
+
if (keep)
|
|
126
|
+
extracted.push(normalized);
|
|
127
|
+
return keep;
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
(0, sqlite_restore_js_1.applySqliteRestore)(dataDir, (0, sqlite_restore_js_1.planSqliteRestore)(extracted), log);
|
|
131
|
+
// A database the archive promised and the extraction did not write was NOT
|
|
132
|
+
// restored. Say so before "Restore complete" — silence would read as done.
|
|
133
|
+
const { missing, outOfScope } = (0, sqlite_restore_js_1.missingPromisedSnapshots)(promised, extracted, inScope);
|
|
134
|
+
for (const rel of missing) {
|
|
135
|
+
log(`[launcher] WARNING Restore: the archive promised the database snapshot ${rel} but carried none — that database was NOT restored`);
|
|
136
|
+
}
|
|
137
|
+
for (const rel of outOfScope) {
|
|
138
|
+
log(`[launcher] Restore: the database snapshot ${rel} is outside the chosen locations — that database was left as it was`);
|
|
139
|
+
}
|
|
140
|
+
fs.rmSync(markerPath, { force: true });
|
|
141
|
+
log('[launcher] Restore complete; marker cleared');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The `sqliteSnapshots[].snapshotPath` the archive's manifest lists. Reads only
|
|
145
|
+
* up to the manifest (the first entry) and stops. An archive with no manifest,
|
|
146
|
+
* or one from before D693, promises nothing.
|
|
147
|
+
*/
|
|
148
|
+
function readPromisedSnapshots(archivePath, log) {
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
let settled = false;
|
|
151
|
+
const done = (paths) => {
|
|
152
|
+
if (settled)
|
|
153
|
+
return;
|
|
154
|
+
settled = true;
|
|
155
|
+
reader.destroy();
|
|
156
|
+
resolve(paths);
|
|
157
|
+
};
|
|
158
|
+
const chunks = [];
|
|
159
|
+
const parser = new tar.Parser({
|
|
160
|
+
onReadEntry: (entry) => {
|
|
161
|
+
const name = entry.path.replace(/^\.\//, '');
|
|
162
|
+
if (name !== ARCHIVE_MANIFEST_NAME) {
|
|
163
|
+
entry.resume();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
entry.on('data', (chunk) => chunks.push(chunk));
|
|
167
|
+
entry.on('end', () => done(promisedIn(Buffer.concat(chunks).toString('utf-8'), log)));
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
parser.on('end', () => done([]));
|
|
171
|
+
const reader = fs.createReadStream(archivePath);
|
|
172
|
+
reader.on('error', () => done([]));
|
|
173
|
+
reader.pipe(parser);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function promisedIn(json, log) {
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(json);
|
|
179
|
+
if (typeof parsed !== 'object' || parsed === null || !('sqliteSnapshots' in parsed))
|
|
180
|
+
return [];
|
|
181
|
+
const list = parsed.sqliteSnapshots;
|
|
182
|
+
if (!Array.isArray(list))
|
|
183
|
+
return [];
|
|
184
|
+
const out = [];
|
|
185
|
+
for (const item of list) {
|
|
186
|
+
if (typeof item === 'object' &&
|
|
187
|
+
item !== null &&
|
|
188
|
+
'snapshotPath' in item &&
|
|
189
|
+
typeof item.snapshotPath === 'string') {
|
|
190
|
+
out.push(item.snapshotPath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
log(`[launcher] Restore: the archive manifest could not be read — ${String(err)}`);
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SQLITE_SNAPSHOT_SUFFIX = void 0;
|
|
37
|
+
exports.planSqliteRestore = planSqliteRestore;
|
|
38
|
+
exports.applySqliteRestore = applySqliteRestore;
|
|
39
|
+
exports.missingPromisedSnapshots = missingPromisedSnapshots;
|
|
40
|
+
/**
|
|
41
|
+
* The SQLite half of the boot-time restore (D693). Runs in the launcher,
|
|
42
|
+
* BEFORE any runner exists — on databases nobody has open, which is the one
|
|
43
|
+
* place a filesystem call may touch a `.db` / `-wal` / `-shm` (the D679 guard
|
|
44
|
+
* names this file in its allowlist, with that reason).
|
|
45
|
+
*
|
|
46
|
+
* Two jobs, both about the sidecars SQLite pairs a database with on its first
|
|
47
|
+
* open:
|
|
48
|
+
*
|
|
49
|
+
* 1. **Promote an owner snapshot.** An archive made since D693 carries
|
|
50
|
+
* `<db>.backup-snapshot` in place of the live database. It is a verified,
|
|
51
|
+
* self-contained copy (rollback journal, no sidecars). Restoring it means:
|
|
52
|
+
* delete the target's `<db>-wal`, `-shm` and `-journal`, then rename the
|
|
53
|
+
* snapshot over `<db>`.
|
|
54
|
+
* 2. **Guard a legacy archive.** An archive from before D693 carries `<db>`
|
|
55
|
+
* as a file, sometimes without its `-wal`. Whatever sidecar the archive did
|
|
56
|
+
* NOT bring is deleted from the target.
|
|
57
|
+
*
|
|
58
|
+
* Why the sidecars must go: a `-wal` left by the database being replaced (a
|
|
59
|
+
* crash, a kill, a runner that never checkpointed) has valid frames, and
|
|
60
|
+
* SQLite replays them onto whatever file sits beside it. The restored copy
|
|
61
|
+
* would come up as a mix of two databases.
|
|
62
|
+
*
|
|
63
|
+
* Zero `@camstack/*` runtime imports, like the launcher that calls it. The
|
|
64
|
+
* suffix is spelled here and pinned to `DATABASE_SNAPSHOT_SUFFIX` by
|
|
65
|
+
* `pending-restore-sqlite.spec.ts`.
|
|
66
|
+
*/
|
|
67
|
+
const fs = __importStar(require("node:fs"));
|
|
68
|
+
const path = __importStar(require("node:path"));
|
|
69
|
+
/** Mirrors `DATABASE_SNAPSHOT_SUFFIX` in `@camstack/types` (spec-pinned). */
|
|
70
|
+
exports.SQLITE_SNAPSHOT_SUFFIX = '.backup-snapshot';
|
|
71
|
+
const SIDECARS = ['-wal', '-shm', '-journal'];
|
|
72
|
+
const DATABASE_SUFFIXES = ['.db', '.sqlite', '.sqlite3'];
|
|
73
|
+
/** Decide, from the paths an extraction wrote, what the target needs. */
|
|
74
|
+
function planSqliteRestore(extracted) {
|
|
75
|
+
const written = new Set(extracted);
|
|
76
|
+
const promote = [];
|
|
77
|
+
const dropSidecars = new Map();
|
|
78
|
+
for (const rel of written) {
|
|
79
|
+
if (rel.endsWith(exports.SQLITE_SNAPSHOT_SUFFIX)) {
|
|
80
|
+
const database = rel.slice(0, -exports.SQLITE_SNAPSHOT_SUFFIX.length);
|
|
81
|
+
promote.push(rel);
|
|
82
|
+
// Whatever sits beside the target belongs to the database being replaced.
|
|
83
|
+
dropSidecars.set(database, [...SIDECARS]);
|
|
84
|
+
}
|
|
85
|
+
else if (DATABASE_SUFFIXES.some((suffix) => rel.endsWith(suffix))) {
|
|
86
|
+
const missing = SIDECARS.filter((sidecar) => !written.has(rel + sidecar));
|
|
87
|
+
if (missing.length > 0 && !dropSidecars.has(rel))
|
|
88
|
+
dropSidecars.set(rel, missing);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { promote: promote.sort(), dropSidecars };
|
|
92
|
+
}
|
|
93
|
+
/** Make the entries just removed or renamed in `dir` durable. */
|
|
94
|
+
function fsyncDirectory(dir) {
|
|
95
|
+
const fd = fs.openSync(dir, 'r');
|
|
96
|
+
try {
|
|
97
|
+
fs.fsyncSync(fd);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
fs.closeSync(fd);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Apply a plan under `dataDir`. Each deletion and promotion is one log line,
|
|
105
|
+
* and each touched directory is fsynced before the caller clears its marker —
|
|
106
|
+
* a power cut after "Restore complete" must not bring the stale WAL back.
|
|
107
|
+
*/
|
|
108
|
+
function applySqliteRestore(dataDir, plan, log, syncDirectory = fsyncDirectory) {
|
|
109
|
+
const touched = new Set();
|
|
110
|
+
for (const [database, sidecars] of plan.dropSidecars) {
|
|
111
|
+
for (const sidecar of sidecars) {
|
|
112
|
+
const stale = path.join(dataDir, database + sidecar);
|
|
113
|
+
if (!fs.existsSync(stale))
|
|
114
|
+
continue;
|
|
115
|
+
fs.rmSync(stale, { force: true });
|
|
116
|
+
touched.add(path.dirname(stale));
|
|
117
|
+
log(`[launcher] Restore: dropped stale ${database}${sidecar} — it belonged to the database being replaced`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const snapshot of plan.promote) {
|
|
121
|
+
const database = snapshot.slice(0, -exports.SQLITE_SNAPSHOT_SUFFIX.length);
|
|
122
|
+
fs.renameSync(path.join(dataDir, snapshot), path.join(dataDir, database));
|
|
123
|
+
touched.add(path.dirname(path.join(dataDir, database)));
|
|
124
|
+
log(`[launcher] Restore: promoted the owner snapshot ${snapshot} → ${database}`);
|
|
125
|
+
}
|
|
126
|
+
for (const dir of touched) {
|
|
127
|
+
// A durability nicety, never a reason to abort boot — a launcher that
|
|
128
|
+
// threw here would fail every boot while the marker stays.
|
|
129
|
+
try {
|
|
130
|
+
syncDirectory(dir);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
log(`[launcher] WARNING Restore: could not fsync ${dir} (${String(err)}) — continuing`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function missingPromisedSnapshots(promised, extracted, inScope) {
|
|
138
|
+
const written = new Set(extracted);
|
|
139
|
+
const missing = [];
|
|
140
|
+
const outOfScope = [];
|
|
141
|
+
for (const rel of promised) {
|
|
142
|
+
if (written.has(rel))
|
|
143
|
+
continue;
|
|
144
|
+
if (inScope(rel))
|
|
145
|
+
missing.push(rel);
|
|
146
|
+
else
|
|
147
|
+
outOfScope.push(rel);
|
|
148
|
+
}
|
|
149
|
+
return { missing, outOfScope };
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.427",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -37,17 +37,17 @@
|
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
40
|
+
"@camstack/addon-admin-ui": "1.2.297",
|
|
41
41
|
"@camstack/addon-agent-ui": "1.2.159",
|
|
42
42
|
"@camstack/addon-auth": "1.2.123",
|
|
43
43
|
"@camstack/addon-decoder-nodeav": "1.2.115",
|
|
44
44
|
"@camstack/addon-notifiers": "1.2.121",
|
|
45
45
|
"@camstack/addon-pipeline": "1.2.313",
|
|
46
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
47
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
46
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.269",
|
|
47
|
+
"@camstack/addon-post-analysis": "1.2.307",
|
|
48
48
|
"@camstack/sdk": "1.2.118",
|
|
49
|
-
"@camstack/system": "1.2.
|
|
50
|
-
"@camstack/types": "1.2.
|
|
49
|
+
"@camstack/system": "1.2.329",
|
|
50
|
+
"@camstack/types": "1.2.282",
|
|
51
51
|
"@camstack/ui-library": "1.2.217",
|
|
52
52
|
"@fastify/compress": "^9.0.0",
|
|
53
53
|
"@fastify/cookie": "^11.0.2",
|