@better-forge/backup 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -3
  3. package/dist/archive.d.ts +17 -0
  4. package/dist/archive.d.ts.map +1 -0
  5. package/dist/archive.js +132 -0
  6. package/dist/archive.js.map +1 -0
  7. package/dist/cli.d.ts +28 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +54 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/index.d.ts +8 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +8 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/restore.d.ts +42 -0
  16. package/dist/restore.d.ts.map +1 -0
  17. package/dist/restore.js +89 -0
  18. package/dist/restore.js.map +1 -0
  19. package/dist/routes.d.ts +23 -0
  20. package/dist/routes.d.ts.map +1 -0
  21. package/dist/routes.js +111 -0
  22. package/dist/routes.js.map +1 -0
  23. package/dist/runner.d.ts +30 -0
  24. package/dist/runner.d.ts.map +1 -0
  25. package/dist/runner.js +26 -0
  26. package/dist/runner.js.map +1 -0
  27. package/dist/scheduler.d.ts +16 -0
  28. package/dist/scheduler.d.ts.map +1 -0
  29. package/dist/scheduler.js +27 -0
  30. package/dist/scheduler.js.map +1 -0
  31. package/dist/service.d.ts +79 -0
  32. package/dist/service.d.ts.map +1 -0
  33. package/dist/service.js +244 -0
  34. package/dist/service.js.map +1 -0
  35. package/dist/worker.d.ts +2 -0
  36. package/dist/worker.d.ts.map +1 -0
  37. package/dist/worker.js +12 -0
  38. package/dist/worker.js.map +1 -0
  39. package/package.json +57 -6
  40. package/src/archive.ts +114 -0
  41. package/src/cli.ts +62 -0
  42. package/src/index.ts +10 -0
  43. package/src/restore.ts +111 -0
  44. package/src/routes.ts +115 -0
  45. package/src/runner.ts +45 -0
  46. package/src/scheduler.ts +39 -0
  47. package/src/service.ts +250 -0
  48. package/src/worker.ts +18 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BetterStatusPage contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,12 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @better-forge/backup
2
+
3
+ Backup, restore and verification of a SQLite database and the files around it.
4
+
5
+ - `createBackupService` writes a consistent snapshot of the database (`VACUUM INTO`, integrity-checked) plus the files the application adds, into one checksummed archive, and verifies, lists, prunes and locks backups. The schedule and the last status are kept as files beside the backups.
6
+ - `createRestore` restores an archive over a stopped application, takes a safety backup first and puts every replaced file back if the restore fails half way. The application supplies the files around the database, its database handle and the checks that may refuse a backup.
7
+ - `createBackupRunner` and `@better-forge/backup/worker` run a backup or a verification on a worker thread.
8
+ - `createBackupRoutes` makes the admin routes (list, create, schedule, download, delete, verify an uploaded file) and `createBackupScheduler` runs backups on the configured schedule.
9
+ - `runBackupCli` and `runRestoreCli` are the command lines of `backup`, `backup --verify` and `restore`; the application keeps its boot (environment, database) in the entry file.
10
+ - `createArchiveFormat` is the archive format on its own; the `magic` bytes identify the archives of one product.
11
+
12
+ Depends on `@better-forge/audit` (the routes) and `node-cron` (the scheduler); the service itself needs only Node (`node:sqlite`).
@@ -0,0 +1,17 @@
1
+ export interface ArchiveEntry {
2
+ name: string;
3
+ source: string;
4
+ }
5
+ /**
6
+ * The archive file format: a magic header, then for each file its name, size and SHA-256 followed by its bytes.
7
+ * Extraction checks every checksum and refuses names that would leave the destination. The `magic` is what identifies
8
+ * archives of one product, so an application keeps the one its existing archives start with.
9
+ */
10
+ export declare function createArchiveFormat(options: {
11
+ magic: string;
12
+ }): {
13
+ createArchive: (output: string, entries: ArchiveEntry[]) => void;
14
+ extractArchive: (input: string, destination: string) => string[];
15
+ };
16
+ export type ArchiveFormat = ReturnType<typeof createArchiveFormat>;
17
+ //# sourceMappingURL=archive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,YAAY;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AA6C9D;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE;4BAG7B,MAAM,WAAW,YAAY,EAAE,KAAG,IAAI;4BAsBtC,MAAM,eAAe,MAAM,KAAG,MAAM,EAAE;EA8BtE;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAA"}
@@ -0,0 +1,132 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ const HASH_SIZE = 32;
5
+ function safeName(name) {
6
+ const normalized = name.replaceAll('\\', '/');
7
+ if (!normalized || normalized.startsWith('/') || normalized.includes('../') || normalized === '..') {
8
+ throw new Error(`Unsafe archive path: ${name}`);
9
+ }
10
+ return normalized;
11
+ }
12
+ function sha256(file) {
13
+ const hash = crypto.createHash('sha256');
14
+ const fd = fs.openSync(file, 'r');
15
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
16
+ try {
17
+ let count = 0;
18
+ while ((count = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0)
19
+ hash.update(buffer.subarray(0, count));
20
+ }
21
+ finally {
22
+ fs.closeSync(fd);
23
+ }
24
+ return hash.digest();
25
+ }
26
+ function copyBytes(input, output, size, hash) {
27
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
28
+ let remaining = size;
29
+ while (remaining > 0) {
30
+ const count = fs.readSync(input, buffer, 0, Math.min(buffer.length, remaining), null);
31
+ if (count === 0)
32
+ throw new Error('Unexpected end of backup archive');
33
+ const chunk = buffer.subarray(0, count);
34
+ if (hash)
35
+ hash.update(chunk);
36
+ fs.writeSync(output, chunk);
37
+ remaining -= count;
38
+ }
39
+ }
40
+ function readExact(fd, size) {
41
+ const buffer = Buffer.alloc(size);
42
+ let offset = 0;
43
+ while (offset < size) {
44
+ const count = fs.readSync(fd, buffer, offset, size - offset, null);
45
+ if (!count)
46
+ throw new Error('Truncated backup archive');
47
+ offset += count;
48
+ }
49
+ return buffer;
50
+ }
51
+ /**
52
+ * The archive file format: a magic header, then for each file its name, size and SHA-256 followed by its bytes.
53
+ * Extraction checks every checksum and refuses names that would leave the destination. The `magic` is what identifies
54
+ * archives of one product, so an application keeps the one its existing archives start with.
55
+ */
56
+ export function createArchiveFormat(options) {
57
+ const MAGIC = Buffer.from(options.magic);
58
+ function createArchive(output, entries) {
59
+ const fd = fs.openSync(output, 'wx', 0o600);
60
+ try {
61
+ fs.writeSync(fd, MAGIC);
62
+ for (const entry of entries) {
63
+ const name = Buffer.from(safeName(entry.name), 'utf8');
64
+ const stat = fs.statSync(entry.source);
65
+ if (!stat.isFile())
66
+ throw new Error(`Archive source is not a file: ${entry.source}`);
67
+ const header = Buffer.alloc(4 + 8 + HASH_SIZE);
68
+ header.writeUInt32BE(name.length, 0);
69
+ header.writeBigUInt64BE(BigInt(stat.size), 4);
70
+ sha256(entry.source).copy(header, 12);
71
+ fs.writeSync(fd, header);
72
+ fs.writeSync(fd, name);
73
+ const input = fs.openSync(entry.source, 'r');
74
+ try {
75
+ copyBytes(input, fd, stat.size);
76
+ }
77
+ finally {
78
+ fs.closeSync(input);
79
+ }
80
+ }
81
+ fs.writeSync(fd, Buffer.alloc(4));
82
+ fs.fsyncSync(fd);
83
+ }
84
+ finally {
85
+ fs.closeSync(fd);
86
+ }
87
+ }
88
+ function extractArchive(input, destination) {
89
+ fs.mkdirSync(destination, { recursive: true, mode: 0o700 });
90
+ const root = path.resolve(destination);
91
+ const extracted = [];
92
+ const fd = fs.openSync(input, 'r');
93
+ try {
94
+ if (!readExact(fd, MAGIC.length).equals(MAGIC))
95
+ throw new Error('Unsupported backup format');
96
+ for (let index = 0; index < 10000; index++) {
97
+ const nameLength = readExact(fd, 4).readUInt32BE(0);
98
+ if (nameLength === 0)
99
+ return extracted;
100
+ if (nameLength > 4096)
101
+ throw new Error('Invalid archive entry name');
102
+ const metadata = readExact(fd, 8 + HASH_SIZE);
103
+ const sizeBig = metadata.readBigUInt64BE(0);
104
+ if (sizeBig > BigInt(Number.MAX_SAFE_INTEGER))
105
+ throw new Error('Archive entry is too large');
106
+ const expectedHash = metadata.subarray(8);
107
+ const name = safeName(readExact(fd, nameLength).toString('utf8'));
108
+ const target = path.resolve(root, ...name.split('/'));
109
+ if (target !== root && !target.startsWith(`${root}${path.sep}`))
110
+ throw new Error('Archive path escapes destination');
111
+ fs.mkdirSync(path.dirname(target), { recursive: true });
112
+ const output = fs.openSync(target, 'wx', 0o600);
113
+ const hash = crypto.createHash('sha256');
114
+ try {
115
+ copyBytes(fd, output, Number(sizeBig), hash);
116
+ }
117
+ finally {
118
+ fs.closeSync(output);
119
+ }
120
+ if (!crypto.timingSafeEqual(hash.digest(), expectedHash))
121
+ throw new Error(`Checksum mismatch: ${name}`);
122
+ extracted.push(name);
123
+ }
124
+ throw new Error('Archive contains too many entries');
125
+ }
126
+ finally {
127
+ fs.closeSync(fd);
128
+ }
129
+ }
130
+ return { createArchive, extractArchive };
131
+ }
132
+ //# sourceMappingURL=archive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { BackupService } from './service.js';
2
+ export interface BackupCliOptions {
3
+ service: Pick<BackupService, 'createBackup' | 'validateBackup'>;
4
+ /** The application's database handle: opened for the backup and closed after it. */
5
+ database: {
6
+ open: () => void;
7
+ close: () => void;
8
+ };
9
+ /** A backup file name as the usage line shows it (`<name>-backup-timestamp.backup`). */
10
+ exampleFile: string;
11
+ }
12
+ /**
13
+ * The command line of `backup`: makes a backup (`--output <dir>` picks where) or, with `--verify <file>` /
14
+ * `--verify --input <file>`, checks one. Prints JSON; failures set a non-zero exit code.
15
+ */
16
+ export declare function runBackupCli(argv: string[], options: BackupCliOptions): Promise<void>;
17
+ export interface RestoreCliOptions {
18
+ restore: (input: string, extra: string) => Promise<unknown>;
19
+ exampleFile: string;
20
+ /** The one extra value the restore needs (for example the key the backup was made with). */
21
+ extra: {
22
+ flag: string;
23
+ placeholder: string;
24
+ };
25
+ }
26
+ /** The command line of `restore`: `--input <file>` and the extra flag are both required. */
27
+ export declare function runRestoreCli(argv: string[], options: RestoreCliOptions): Promise<void>;
28
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAYjD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,cAAc,GAAG,gBAAgB,CAAC,CAAA;IAC/D,oFAAoF;IACpF,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;IACjD,wFAAwF;IACxF,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB3F;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3D,WAAW,EAAE,MAAM,CAAA;IACnB,4FAA4F;IAC5F,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAC7C;AAED,4FAA4F;AAC5F,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU7F"}
package/dist/cli.js ADDED
@@ -0,0 +1,54 @@
1
+ import path from 'node:path';
2
+ function argument(argv, name) {
3
+ const index = argv.indexOf(name);
4
+ return index >= 0 ? argv[index + 1] : undefined;
5
+ }
6
+ function fail(error) {
7
+ console.error(error instanceof Error ? error.message : error);
8
+ process.exitCode = 1;
9
+ }
10
+ /**
11
+ * The command line of `backup`: makes a backup (`--output <dir>` picks where) or, with `--verify <file>` /
12
+ * `--verify --input <file>`, checks one. Prints JSON; failures set a non-zero exit code.
13
+ */
14
+ export async function runBackupCli(argv, options) {
15
+ try {
16
+ const verifyMode = argv.includes('--verify');
17
+ const verify = argument(argv, '--input') ?? argument(argv, '--verify');
18
+ if (verifyMode) {
19
+ if (!verify || verify.startsWith('--'))
20
+ throw new Error(`Usage: backup:verify -- --input <${options.exampleFile}>`);
21
+ const manifest = options.service.validateBackup(path.resolve(verify));
22
+ console.log(JSON.stringify({ valid: true, manifest }, null, 2));
23
+ return;
24
+ }
25
+ options.database.open();
26
+ try {
27
+ const output = argument(argv, '--output');
28
+ console.log(JSON.stringify(await options.service.createBackup(output ? path.resolve(output) : undefined), null, 2));
29
+ }
30
+ finally {
31
+ options.database.close();
32
+ }
33
+ }
34
+ catch (error) {
35
+ fail(error);
36
+ }
37
+ }
38
+ /** The command line of `restore`: `--input <file>` and the extra flag are both required. */
39
+ export async function runRestoreCli(argv, options) {
40
+ const input = argument(argv, '--input');
41
+ const extra = argument(argv, options.extra.flag);
42
+ if (!input || !extra) {
43
+ console.error(`Usage: restore --input <${options.exampleFile}> ${options.extra.flag} ${options.extra.placeholder}`);
44
+ process.exitCode = 1;
45
+ return;
46
+ }
47
+ try {
48
+ console.log(JSON.stringify(await options.restore(path.resolve(input), extra), null, 2));
49
+ }
50
+ catch (error) {
51
+ fail(error);
52
+ }
53
+ }
54
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export { createArchiveFormat, type ArchiveEntry, type ArchiveFormat } from './archive.js';
2
+ export { collectFiles, createBackupService, DEFAULT_BACKUP_CONFIG, DEFAULT_BACKUP_STATUS, INTERRUPTED_BACKUP_ERROR, type BackupConfig, type BackupInfo, type BackupManifest, type BackupService, type BackupServiceOptions, type BackupStatus, type ProcessOwner, } from './service.js';
3
+ export { createRestore, replaceFile, type RestoreExtras, type RestoreOptions, type RestoreResult } from './restore.js';
4
+ export { createBackupRunner, spawnBackupWorker, type BackupJob, type BackupWorkerOptions } from './runner.js';
5
+ export { createBackupScheduler, scheduleExpression, type BackupSchedulerOptions } from './scheduler.js';
6
+ export { createBackupRoutes, type BackupRoutesOptions } from './routes.js';
7
+ export { runBackupCli, runRestoreCli, type BackupCliOptions, type RestoreCliOptions } from './cli.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AACzF,OAAO,EACL,YAAY,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,wBAAwB,EACzG,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,GAC7I,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AACtH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,KAAK,SAAS,EAAE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAC7G,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvG,OAAO,EAAE,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAC1E,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { createArchiveFormat } from './archive.js';
2
+ export { collectFiles, createBackupService, DEFAULT_BACKUP_CONFIG, DEFAULT_BACKUP_STATUS, INTERRUPTED_BACKUP_ERROR, } from './service.js';
3
+ export { createRestore, replaceFile } from './restore.js';
4
+ export { createBackupRunner, spawnBackupWorker } from './runner.js';
5
+ export { createBackupScheduler, scheduleExpression } from './scheduler.js';
6
+ export { createBackupRoutes } from './routes.js';
7
+ export { runBackupCli, runRestoreCli } from './cli.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAyC,MAAM,cAAc,CAAA;AACzF,OAAO,EACL,YAAY,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,wBAAwB,GAE1G,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,aAAa,EAAE,WAAW,EAA+D,MAAM,cAAc,CAAA;AACtH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAA4C,MAAM,aAAa,CAAA;AAC7G,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAA+B,MAAM,gBAAgB,CAAA;AACvG,OAAO,EAAE,kBAAkB,EAA4B,MAAM,aAAa,CAAA;AAC1E,OAAO,EAAE,YAAY,EAAE,aAAa,EAAiD,MAAM,UAAU,CAAA"}
@@ -0,0 +1,42 @@
1
+ import type { BackupManifest, BackupService } from './service.js';
2
+ export interface RestoreResult {
3
+ restoredAt: number;
4
+ safetyBackup: string | null;
5
+ }
6
+ /** Puts `source` at `target` by copying beside it and renaming, so a failure never leaves half a file. */
7
+ export declare function replaceFile(source: string, target: string): void;
8
+ /** The files around the database that a restore replaces, and how to put the old ones back. */
9
+ export interface RestoreExtras {
10
+ /** Copies what is there now into `rollbackDir`. */
11
+ save: (rollbackDir: string) => void;
12
+ /** Replaces what is there with what the backup holds (extracted into `extractedDir`). */
13
+ apply: (extractedDir: string) => void;
14
+ /** Puts back what `save` kept (and removes what the failed restore created). */
15
+ rollback: (rollbackDir: string) => void;
16
+ }
17
+ export interface RestoreOptions<Manifest extends BackupManifest> {
18
+ service: BackupService<Manifest>;
19
+ name: string;
20
+ /** How the application is called in the messages shown to its operator. */
21
+ displayName: string;
22
+ databasePath: () => string;
23
+ /** Throws unless the application is stopped. */
24
+ assertStopped: () => void;
25
+ /** The application's own database handle: opened to take the safety backup and to migrate what was restored. */
26
+ database: {
27
+ open: () => void;
28
+ close: () => void;
29
+ migrate: () => void;
30
+ };
31
+ /** Called once the restore is about to replace files; the flags it captures describe what existed before. */
32
+ extras: () => RestoreExtras;
33
+ }
34
+ /**
35
+ * Restores a backup over a stopped application. A safety backup of what is there now is taken first, and a failure
36
+ * half way puts every replaced file back.
37
+ */
38
+ export declare function createRestore<Manifest extends BackupManifest>(options: RestoreOptions<Manifest>): (input: string, hooks?: {
39
+ prepare?: () => void;
40
+ check?: (manifest: Manifest) => void;
41
+ }) => Promise<RestoreResult>;
42
+ //# sourceMappingURL=restore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore.d.ts","sourceRoot":"","sources":["../src/restore.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAEjE,MAAM,WAAW,aAAa;IAAG,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AAElF,0GAA0G;AAC1G,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAKhE;AAOD,+FAA+F;AAC/F,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,yFAAyF;IACzF,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,gFAAgF;IAChF,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;CACxC;AAED,MAAM,WAAW,cAAc,CAAC,QAAQ,SAAS,cAAc;IAC7D,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,MAAM,CAAA;IAC1B,gDAAgD;IAChD,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,gHAAgH;IAChH,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;IACtE,6GAA6G;IAC7G,MAAM,EAAE,MAAM,aAAa,CAAA;CAC5B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,SAAS,cAAc,EAAE,OAAO,EAAE,cAAc,CAAC,QAAQ,CAAC,IAQ5F,OAAO,MAAM,EACb,QAAO;IAAE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAO,KACzE,OAAO,CAAC,aAAa,CAAC,CAmD1B"}
@@ -0,0 +1,89 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ /** Puts `source` at `target` by copying beside it and renaming, so a failure never leaves half a file. */
6
+ export function replaceFile(source, target) {
7
+ fs.mkdirSync(path.dirname(target), { recursive: true });
8
+ const incoming = `${target}.restore-incoming`;
9
+ fs.copyFileSync(source, incoming);
10
+ fs.renameSync(incoming, target);
11
+ }
12
+ function removeSqliteSidecars(dbPath) {
13
+ fs.rmSync(`${dbPath}-wal`, { force: true });
14
+ fs.rmSync(`${dbPath}-shm`, { force: true });
15
+ }
16
+ /**
17
+ * Restores a backup over a stopped application. A safety backup of what is there now is taken first, and a failure
18
+ * half way puts every replaced file back.
19
+ */
20
+ export function createRestore(options) {
21
+ const { service, database } = options;
22
+ /**
23
+ * `prepare` runs before the backup is read (to set up the key it will need); `check` may refuse the backup by
24
+ * throwing, once its manifest is known.
25
+ */
26
+ return async function restoreBackup(input, hooks = {}) {
27
+ options.assertStopped();
28
+ hooks.prepare?.();
29
+ const manifest = service.validateBackup(input);
30
+ hooks.check?.(manifest);
31
+ const dbPath = options.databasePath();
32
+ if (fs.existsSync(dbPath)) {
33
+ const lockCheck = new DatabaseSync(dbPath);
34
+ try {
35
+ lockCheck.exec('PRAGMA busy_timeout=0; BEGIN EXCLUSIVE; ROLLBACK');
36
+ }
37
+ catch {
38
+ lockCheck.close();
39
+ throw new Error(`Database is in use. Stop ${options.displayName} before restoring.`);
40
+ }
41
+ lockCheck.close();
42
+ }
43
+ let safetyBackup = null;
44
+ if (fs.existsSync(dbPath)) {
45
+ database.open();
46
+ try {
47
+ safetyBackup = (await service.createBackup()).filename;
48
+ }
49
+ finally {
50
+ database.close();
51
+ }
52
+ }
53
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), `${options.name}-restore-`));
54
+ const rollback = fs.mkdtempSync(path.join(os.tmpdir(), `${options.name}-rollback-`));
55
+ const hadDatabase = fs.existsSync(dbPath);
56
+ const extras = options.extras();
57
+ try {
58
+ service.extractArchive(input, temp);
59
+ if (fs.existsSync(dbPath))
60
+ fs.copyFileSync(dbPath, path.join(rollback, 'database.sqlite'));
61
+ extras.save(rollback);
62
+ try {
63
+ removeSqliteSidecars(dbPath);
64
+ replaceFile(path.join(temp, 'database.sqlite'), dbPath);
65
+ extras.apply(temp);
66
+ database.open();
67
+ database.migrate();
68
+ database.close();
69
+ }
70
+ catch (error) {
71
+ database.close();
72
+ removeSqliteSidecars(dbPath);
73
+ const oldDb = path.join(rollback, 'database.sqlite');
74
+ if (fs.existsSync(oldDb))
75
+ replaceFile(oldDb, dbPath);
76
+ else if (!hadDatabase)
77
+ fs.rmSync(dbPath, { force: true });
78
+ extras.rollback(rollback);
79
+ throw error;
80
+ }
81
+ return { restoredAt: Date.now(), safetyBackup };
82
+ }
83
+ finally {
84
+ fs.rmSync(temp, { recursive: true, force: true });
85
+ fs.rmSync(rollback, { recursive: true, force: true });
86
+ }
87
+ };
88
+ }
89
+ //# sourceMappingURL=restore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore.js","sourceRoot":"","sources":["../src/restore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import type { Audit } from '@better-forge/audit';
3
+ import { type BackupInfo, type BackupManifest, type BackupService } from './service.js';
4
+ export interface BackupRoutesOptions<Manifest extends BackupManifest> {
5
+ service: BackupService<Manifest>;
6
+ audit: Audit;
7
+ /** The signed-in caller of a request (the routes sit behind the application's own guards). */
8
+ identity: (req: FastifyRequest) => {
9
+ userId: number;
10
+ email: string;
11
+ };
12
+ /** Makes a backup (on a worker thread, in the application). */
13
+ createBackup: () => Promise<BackupInfo>;
14
+ /** Verifies a backup file (on a worker thread, in the application). */
15
+ validateBackup: (input: string) => Promise<Manifest>;
16
+ /** Called after the schedule changed, to apply it. */
17
+ restartScheduler: () => void;
18
+ /** Fields added to the answer of a verification, next to the manifest (e.g. whether a key matches). */
19
+ describeManifest?: (manifest: Manifest) => Record<string, unknown>;
20
+ }
21
+ /** The admin routes of the backups: list, create, schedule, download, delete and verify an uploaded file. */
22
+ export declare function createBackupRoutes<Manifest extends BackupManifest>(options: BackupRoutesOptions<Manifest>): (app: FastifyInstance) => Promise<void>;
23
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAM9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,EAA4C,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAMjI,MAAM,WAAW,mBAAmB,CAAC,QAAQ,SAAS,cAAc;IAClE,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;IAChC,KAAK,EAAE,KAAK,CAAA;IACZ,8FAA8F;IAC9F,QAAQ,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACpE,+DAA+D;IAC/D,YAAY,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAA;IACvC,uEAAuE;IACvE,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IACpD,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,uGAAuG;IACvG,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnE;AAiBD,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,QAAQ,SAAS,cAAc,EAAE,OAAO,EAAE,mBAAmB,CAAC,QAAQ,CAAC,IAIrE,KAAK,eAAe,mBAkExD"}
package/dist/routes.js ADDED
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { DEFAULT_BACKUP_CONFIG } from './service.js';
6
+ /** Largest backup file the verification accepts. */
7
+ const MAX_VALIDATION_BYTES = 2 * 1024 * 1024 * 1024;
8
+ function parseConfig(value) {
9
+ const config = { ...DEFAULT_BACKUP_CONFIG, ...value };
10
+ if (!['daily', 'weekly'].includes(config.frequency))
11
+ throw new Error('Invalid frequency');
12
+ if (!Number.isInteger(config.hour) || config.hour < 0 || config.hour > 23)
13
+ throw new Error('Hour must be between 0 and 23');
14
+ if (!Number.isInteger(config.minute) || config.minute < 0 || config.minute > 59)
15
+ throw new Error('Minute must be between 0 and 59');
16
+ if (!Number.isInteger(config.weekday) || config.weekday < 0 || config.weekday > 6)
17
+ throw new Error('Weekday must be between 0 and 6');
18
+ if (!Number.isInteger(config.retention) || config.retention < 1 || config.retention > 365)
19
+ throw new Error('Retention must be between 1 and 365');
20
+ return config;
21
+ }
22
+ const messageOf = (error) => error instanceof Error ? error.message : String(error);
23
+ /** The admin routes of the backups: list, create, schedule, download, delete and verify an uploaded file. */
24
+ export function createBackupRoutes(options) {
25
+ const { service, audit } = options;
26
+ const actor = (req) => audit.actor(options.identity(req));
27
+ return async function backupRoutes(app) {
28
+ app.get('/', async () => ({ backups: service.listBackups(), config: service.readBackupConfig(), status: service.readBackupStatus() }));
29
+ app.post('/', async (req, reply) => {
30
+ try {
31
+ const result = await options.createBackup();
32
+ service.applyRetention(service.readBackupConfig().retention);
33
+ await audit.write(actor(req), 'create', 'backup', result.filename, result.filename);
34
+ return result;
35
+ }
36
+ catch (error) {
37
+ return reply.code(409).send({ error: messageOf(error) });
38
+ }
39
+ });
40
+ app.put('/config', async (req, reply) => {
41
+ try {
42
+ const config = parseConfig(req.body);
43
+ service.writeBackupConfig(config);
44
+ service.applyRetention(config.retention);
45
+ options.restartScheduler();
46
+ await audit.write(actor(req), 'update', 'backup-config', null, 'Backup schedule', {
47
+ enabled: config.enabled,
48
+ frequency: config.frequency,
49
+ hour: config.hour,
50
+ minute: config.minute,
51
+ weekday: config.weekday,
52
+ retention: config.retention,
53
+ });
54
+ return config;
55
+ }
56
+ catch (error) {
57
+ return reply.code(400).send({ error: messageOf(error) });
58
+ }
59
+ });
60
+ app.get('/:filename/download', async (req, reply) => {
61
+ try {
62
+ const file = service.backupFile(req.params.filename);
63
+ if (!fs.existsSync(file))
64
+ return reply.code(404).send({ error: 'Backup not found' });
65
+ await audit.write(actor(req), 'allow', 'backup', req.params.filename, req.params.filename, { access: 'downloaded' });
66
+ reply.header('Content-Type', 'application/octet-stream');
67
+ reply.header('Content-Disposition', `attachment; filename="${path.basename(file)}"`);
68
+ return reply.send(fs.createReadStream(file));
69
+ }
70
+ catch (error) {
71
+ return reply.code(400).send({ error: messageOf(error) });
72
+ }
73
+ });
74
+ app.delete('/:filename', async (req, reply) => {
75
+ try {
76
+ if (req.query.confirm !== req.params.filename)
77
+ return reply.code(400).send({ error: 'Type the exact backup filename to confirm deletion' });
78
+ const file = service.backupFile(req.params.filename);
79
+ if (!fs.existsSync(file))
80
+ return reply.code(404).send({ error: 'Backup not found' });
81
+ fs.rmSync(file);
82
+ await audit.write(actor(req), 'delete', 'backup', req.params.filename, req.params.filename);
83
+ return reply.code(204).send();
84
+ }
85
+ catch (error) {
86
+ return reply.code(400).send({ error: messageOf(error) });
87
+ }
88
+ });
89
+ app.post('/validate', async (req, reply) => {
90
+ const data = await req.file({ limits: { fileSize: MAX_VALIDATION_BYTES } });
91
+ if (!data)
92
+ return reply.code(400).send({ error: 'No backup file' });
93
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'backup-upload-'));
94
+ const input = path.join(tempDir, 'backup.backup');
95
+ try {
96
+ await pipeline(data.file, fs.createWriteStream(input, { mode: 0o600 }));
97
+ if (data.file.truncated)
98
+ throw new Error('Backup file exceeds the 2 GB validation limit');
99
+ const manifest = await options.validateBackup(input);
100
+ return { valid: true, manifest, ...options.describeManifest?.(manifest) };
101
+ }
102
+ catch (error) {
103
+ return reply.code(400).send({ error: messageOf(error) });
104
+ }
105
+ finally {
106
+ fs.rmSync(tempDir, { recursive: true, force: true });
107
+ }
108
+ });
109
+ };
110
+ }
111
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}