@tickernelz/wardmote-db 2026.1109.0 → 2026.1112.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.
- package/dist/agent-chat-migration.test.js +2 -2
- package/dist/agent-chat-migration.test.js.map +1 -1
- package/dist/backup-lib.d.ts +38 -0
- package/dist/backup-lib.d.ts.map +1 -1
- package/dist/backup-lib.js +290 -35
- package/dist/backup-lib.js.map +1 -1
- package/dist/backup-lib.test.js +215 -4
- package/dist/backup-lib.test.js.map +1 -1
- package/dist/backup.js +10 -5
- package/dist/backup.js.map +1 -1
- package/dist/better-auth-account-issuer-migration.test.js +1 -1
- package/dist/better-auth-account-issuer-migration.test.js.map +1 -1
- package/dist/built-in-agent-unique-marker-migration.test.js +1 -1
- package/dist/built-in-agent-unique-marker-migration.test.js.map +1 -1
- package/dist/chat-github-migration.test.js +1 -1
- package/dist/chat-github-migration.test.js.map +1 -1
- package/dist/chat-identity-migration-reconciliation.test.js +5 -5
- package/dist/chat-identity-migration-reconciliation.test.js.map +1 -1
- package/dist/chat-openwa-migration.test.js +2 -2
- package/dist/chat-openwa-migration.test.js.map +1 -1
- package/dist/chat-telegram-draft-ids-migration.test.js +1 -1
- package/dist/chat-telegram-draft-ids-migration.test.js.map +1 -1
- package/dist/chat-tenant-foreign-keys.test.js +1 -1
- package/dist/chat-tenant-foreign-keys.test.js.map +1 -1
- package/dist/check-migration-safety.js +2 -2
- package/dist/check-migration-safety.js.map +1 -1
- package/dist/check-migration-safety.test.js +10 -1
- package/dist/check-migration-safety.test.js.map +1 -1
- package/dist/client-options.test.js +6 -6
- package/dist/client-options.test.js.map +1 -1
- package/dist/client.d.ts +1 -22
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -16
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +10 -10
- package/dist/client.test.js.map +1 -1
- package/dist/company-secret-proposals-migration.test.js +1 -1
- package/dist/company-secret-proposals-migration.test.js.map +1 -1
- package/dist/connection-grants-phase4-migration.test.js +1 -1
- package/dist/connection-grants-phase4-migration.test.js.map +1 -1
- package/dist/connections-v3-schema-core-migration.test.js +2 -2
- package/dist/connections-v3-schema-core-migration.test.js.map +1 -1
- package/dist/decision-queue-migrations.test.js +1 -1
- package/dist/decision-queue-migrations.test.js.map +1 -1
- package/dist/embedded-identity.d.ts +12 -0
- package/dist/embedded-identity.d.ts.map +1 -0
- package/dist/embedded-identity.js +96 -0
- package/dist/embedded-identity.js.map +1 -0
- package/dist/embedded-identity.test.d.ts +2 -0
- package/dist/embedded-identity.test.d.ts.map +1 -0
- package/dist/embedded-identity.test.js +91 -0
- package/dist/embedded-identity.test.js.map +1 -0
- package/dist/embedded-postgres-lifecycle.d.ts +0 -9
- package/dist/embedded-postgres-lifecycle.d.ts.map +1 -1
- package/dist/embedded-postgres-lifecycle.js +0 -9
- package/dist/embedded-postgres-lifecycle.js.map +1 -1
- package/dist/embedded-postgres-native.test.js +2 -2
- package/dist/embedded-postgres-native.test.js.map +1 -1
- package/dist/heartbeat-run-issue-privacy-migration.test.js +4 -4
- package/dist/heartbeat-run-issue-privacy-migration.test.js.map +1 -1
- package/dist/inbox-archive-agent-policies-migration.test.js +1 -1
- package/dist/inbox-archive-agent-policies-migration.test.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/issue-comment-derived-attribution-migration.test.js +1 -1
- package/dist/issue-comment-derived-attribution-migration.test.js.map +1 -1
- package/dist/issue-comment-on-behalf-migration.test.js +1 -1
- package/dist/issue-comment-on-behalf-migration.test.js.map +1 -1
- package/dist/migration-runtime.d.ts.map +1 -1
- package/dist/migration-runtime.js +21 -11
- package/dist/migration-runtime.js.map +1 -1
- package/dist/migrations/0359_wardmote_bundled_skill_slugs.sql +100 -0
- package/dist/migrations/0360_wardmote_channel_values.sql +11 -0
- package/dist/migrations/0361_wardmote_stored_modes.sql +14 -0
- package/dist/migrations/0362_wardmote_runner_adapter_type.sql +34 -0
- package/dist/migrations/0363_wardmote_camel_keys.sql +57 -0
- package/dist/migrations/0364_wardmote_catalog_keys.sql +92 -0
- package/dist/migrations/0365_wardmote_plugin_ids.sql +118 -0
- package/dist/migrations/0366_wardmote_managed_environment_key.sql +13 -0
- package/dist/migrations/0367_wardmote_schema_ids.sql +31 -0
- package/dist/migrations/0368_wardmote_mcp_scopes.sql +27 -0
- package/dist/migrations/0369_wardmote_chat_state_key.sql +10 -0
- package/dist/migrations/0370_wardmote_wake_context_key.sql +4 -0
- package/dist/migrations/0371_wardmote_session_config_keys.sql +7 -0
- package/dist/migrations/0372_wardmote_tool_provider_types.sql +23 -0
- package/dist/migrations/meta/_journal.json +98 -0
- package/dist/native-runner-recovery-migration.test.js +1 -1
- package/dist/native-runner-recovery-migration.test.js.map +1 -1
- package/dist/nested-skill-folders-migration.test.js +2 -2
- package/dist/orphaned-claimed-wakeups-migration.test.js +1 -1
- package/dist/orphaned-claimed-wakeups-migration.test.js.map +1 -1
- package/dist/pipelines-schema.test.js +1 -1
- package/dist/pipelines-schema.test.js.map +1 -1
- package/dist/postgres-connection-recovery.test.js +1 -1
- package/dist/postgres-connection-recovery.test.js.map +1 -1
- package/dist/private-task-draft-assets-migration.test.js +1 -1
- package/dist/private-task-draft-assets-migration.test.js.map +1 -1
- package/dist/run-event-payload.js +1 -1
- package/dist/run-event-payload.js.map +1 -1
- package/dist/run-event-payload.test.js +1 -1
- package/dist/run-event-payload.test.js.map +1 -1
- package/dist/run-identity-migration.test.js +1 -1
- package/dist/run-identity-migration.test.js.map +1 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +10 -8
- package/dist/runtime-config.js.map +1 -1
- package/dist/runtime-config.test.js +11 -11
- package/dist/runtime-config.test.js.map +1 -1
- package/dist/schema/chat_channels.d.ts +5 -5
- package/dist/schema/chat_channels.d.ts.map +1 -1
- package/dist/schema/chat_channels.js +4 -19
- package/dist/schema/chat_channels.js.map +1 -1
- package/dist/schema/company_secrets.js +1 -1
- package/dist/schema/company_secrets.js.map +1 -1
- package/dist/schema/environments.js +1 -1
- package/dist/schema/environments.js.map +1 -1
- package/dist/schema/heartbeat_runs.js +1 -1
- package/dist/schema/heartbeat_runs.js.map +1 -1
- package/dist/schema/issue_autonomy_windows.d.ts +2 -2
- package/dist/schema/issue_autonomy_windows.js +1 -1
- package/dist/schema/issue_autonomy_windows.js.map +1 -1
- package/dist/schema/plugin_entities.d.ts +0 -14
- package/dist/schema/plugin_entities.d.ts.map +1 -1
- package/dist/schema/plugin_entities.js +0 -14
- package/dist/schema/plugin_entities.js.map +1 -1
- package/dist/schema/plugin_state.d.ts +0 -23
- package/dist/schema/plugin_state.d.ts.map +1 -1
- package/dist/schema/plugin_state.js +0 -23
- package/dist/schema/plugin_state.js.map +1 -1
- package/dist/schema/plugins.d.ts +3 -3
- package/dist/schema/plugins.d.ts.map +1 -1
- package/dist/schema/plugins.js.map +1 -1
- package/dist/schema/tool_access.d.ts +2 -2
- package/dist/schema/tool_access.js +3 -3
- package/dist/schema/tool_access.js.map +1 -1
- package/dist/schema/user_secret_definitions.js +1 -1
- package/dist/schema/user_secret_definitions.js.map +1 -1
- package/dist/schema/web_push_subscriptions.d.ts +2 -2
- package/dist/session-goal-migration.test.js +3 -3
- package/dist/session-goal-migration.test.js.map +1 -1
- package/dist/speko-voice-migration.test.js +1 -1
- package/dist/speko-voice-migration.test.js.map +1 -1
- package/dist/status-card-migrations.test.js +1 -1
- package/dist/status-card-migrations.test.js.map +1 -1
- package/dist/test-embedded-postgres.d.ts.map +1 -1
- package/dist/test-embedded-postgres.js +9 -8
- package/dist/test-embedded-postgres.js.map +1 -1
- package/dist/test-embedded-postgres.test.js +3 -3
- package/dist/test-embedded-postgres.test.js.map +1 -1
- package/dist/vercel-connect-credential-source-migration.test.js +2 -2
- package/dist/vercel-connect-credential-source-migration.test.js.map +1 -1
- package/dist/wardmote-camel-key-migration.test.d.ts +2 -0
- package/dist/wardmote-camel-key-migration.test.d.ts.map +1 -0
- package/dist/wardmote-camel-key-migration.test.js +111 -0
- package/dist/wardmote-camel-key-migration.test.js.map +1 -0
- package/dist/wardmote-catalog-key-migration.test.d.ts +2 -0
- package/dist/wardmote-catalog-key-migration.test.d.ts.map +1 -0
- package/dist/wardmote-catalog-key-migration.test.js +140 -0
- package/dist/wardmote-catalog-key-migration.test.js.map +1 -0
- package/dist/wardmote-chat-state-key-migration.test.d.ts +2 -0
- package/dist/wardmote-chat-state-key-migration.test.d.ts.map +1 -0
- package/dist/wardmote-chat-state-key-migration.test.js +58 -0
- package/dist/wardmote-chat-state-key-migration.test.js.map +1 -0
- package/dist/wardmote-managed-environment-key-migration.test.d.ts +2 -0
- package/dist/wardmote-managed-environment-key-migration.test.d.ts.map +1 -0
- package/dist/wardmote-managed-environment-key-migration.test.js +67 -0
- package/dist/wardmote-managed-environment-key-migration.test.js.map +1 -0
- package/dist/wardmote-mcp-scope-migration.test.d.ts +2 -0
- package/dist/wardmote-mcp-scope-migration.test.d.ts.map +1 -0
- package/dist/wardmote-mcp-scope-migration.test.js +93 -0
- package/dist/wardmote-mcp-scope-migration.test.js.map +1 -0
- package/dist/wardmote-plugin-id-migration.test.d.ts +2 -0
- package/dist/wardmote-plugin-id-migration.test.d.ts.map +1 -0
- package/dist/wardmote-plugin-id-migration.test.js +127 -0
- package/dist/wardmote-plugin-id-migration.test.js.map +1 -0
- package/dist/wardmote-runner-adapter-type-migration.test.d.ts +2 -0
- package/dist/wardmote-runner-adapter-type-migration.test.d.ts.map +1 -0
- package/dist/wardmote-runner-adapter-type-migration.test.js +124 -0
- package/dist/wardmote-runner-adapter-type-migration.test.js.map +1 -0
- package/dist/wardmote-schema-id-migration.test.d.ts +2 -0
- package/dist/wardmote-schema-id-migration.test.d.ts.map +1 -0
- package/dist/wardmote-schema-id-migration.test.js +113 -0
- package/dist/wardmote-schema-id-migration.test.js.map +1 -0
- package/dist/wardmote-session-config-keys-migration.test.d.ts +2 -0
- package/dist/wardmote-session-config-keys-migration.test.d.ts.map +1 -0
- package/dist/wardmote-session-config-keys-migration.test.js +71 -0
- package/dist/wardmote-session-config-keys-migration.test.js.map +1 -0
- package/dist/wardmote-skill-slug-migration.test.d.ts +2 -0
- package/dist/wardmote-skill-slug-migration.test.d.ts.map +1 -0
- package/dist/wardmote-skill-slug-migration.test.js +128 -0
- package/dist/wardmote-skill-slug-migration.test.js.map +1 -0
- package/dist/wardmote-tool-provider-types-migration.test.d.ts +2 -0
- package/dist/wardmote-tool-provider-types-migration.test.d.ts.map +1 -0
- package/dist/wardmote-tool-provider-types-migration.test.js +92 -0
- package/dist/wardmote-tool-provider-types-migration.test.js.map +1 -0
- package/dist/wardmote-wake-context-key-migration.test.d.ts +2 -0
- package/dist/wardmote-wake-context-key-migration.test.d.ts.map +1 -0
- package/dist/wardmote-wake-context-key-migration.test.js +59 -0
- package/dist/wardmote-wake-context-key-migration.test.js.map +1 -0
- package/package.json +2 -2
|
@@ -55,7 +55,7 @@ async function assertConstraints(sql, row) {
|
|
|
55
55
|
}
|
|
56
56
|
describePostgres("persistent agent chat migration", () => {
|
|
57
57
|
it("applies to a fresh database and enforces conversation identity and message retry uniqueness", async () => {
|
|
58
|
-
const database = await startEmbeddedPostgresTestDatabase("
|
|
58
|
+
const database = await startEmbeddedPostgresTestDatabase("wardmote-chat-migration-fresh-");
|
|
59
59
|
cleanups.push(database.cleanup);
|
|
60
60
|
const sql = postgres(database.connectionString, { max: 1, onnotice: () => { } });
|
|
61
61
|
try {
|
|
@@ -66,7 +66,7 @@ describePostgres("persistent agent chat migration", () => {
|
|
|
66
66
|
}
|
|
67
67
|
}, 30_000);
|
|
68
68
|
it("replays over pre-release columns and constraints without losing history or weakening the state guard", async () => {
|
|
69
|
-
const database = await startEmbeddedPostgresTestDatabase("
|
|
69
|
+
const database = await startEmbeddedPostgresTestDatabase("wardmote-chat-migration-replay-");
|
|
70
70
|
cleanups.push(database.cleanup);
|
|
71
71
|
const sql = postgres(database.connectionString, { max: 1, onnotice: () => { } });
|
|
72
72
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-chat-migration.test.js","sourceRoot":"","sources":["../src/agent-chat-migration.test.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"agent-chat-migration.test.js","sourceRoot":"","sources":["../src/agent-chat-migration.test.ts"],"names":[],"mappings":"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"}
|
package/dist/backup-lib.d.ts
CHANGED
|
@@ -2,6 +2,18 @@ export type BackupRetentionPolicy = {
|
|
|
2
2
|
dailyDays: number;
|
|
3
3
|
weeklyWeeks: number;
|
|
4
4
|
monthlyMonths: number;
|
|
5
|
+
/** Keep only this many newest verified backups; null or absent keeps the daily/weekly/monthly tiers. */
|
|
6
|
+
maxFiles?: number | null;
|
|
7
|
+
};
|
|
8
|
+
export type BackupDiskSpace = {
|
|
9
|
+
freeBytes: number;
|
|
10
|
+
totalBytes: number;
|
|
11
|
+
};
|
|
12
|
+
export type BackupSpaceGuard = {
|
|
13
|
+
reservePercent: number;
|
|
14
|
+
reserveBytes: number;
|
|
15
|
+
headroomFactor?: number;
|
|
16
|
+
statfs?: (dir: string) => BackupDiskSpace;
|
|
5
17
|
};
|
|
6
18
|
export type RunDatabaseBackupOptions = {
|
|
7
19
|
connectionString: string;
|
|
@@ -20,17 +32,43 @@ export type RunDatabaseBackupOptions = {
|
|
|
20
32
|
backupEngine?: "auto" | "pg_dump" | "javascript";
|
|
21
33
|
/** Optional durability gate. A rejected archive must not prune history. */
|
|
22
34
|
verifyBeforePrune?: (backupFile: string) => Promise<void>;
|
|
35
|
+
/** Writes under `<backupDir>/pinned`, outside rotation, for release and rollback dumps. */
|
|
36
|
+
pinned?: boolean;
|
|
37
|
+
/** Deletes pinned backups older than this many days during rotated runs; null or 0 keeps them. */
|
|
38
|
+
pinnedMaxAgeDays?: number | null;
|
|
39
|
+
/** Skips the backup instead of filling the volume when free space is short. */
|
|
40
|
+
spaceGuard?: BackupSpaceGuard | null;
|
|
23
41
|
};
|
|
24
42
|
export type RunDatabaseBackupResult = {
|
|
25
43
|
backupFile: string;
|
|
26
44
|
sizeBytes: number;
|
|
27
45
|
prunedCount: number;
|
|
46
|
+
/** Rotated backups that could not be read; kept, not counted toward maxFiles. */
|
|
47
|
+
unreadableBackups?: string[];
|
|
28
48
|
};
|
|
29
49
|
export type RunDatabaseRestoreOptions = {
|
|
30
50
|
connectionString: string;
|
|
31
51
|
backupFile: string;
|
|
32
52
|
connectTimeoutSeconds?: number;
|
|
33
53
|
};
|
|
54
|
+
export declare const DATABASE_BACKUP_FILENAME_PREFIX = "wardmote";
|
|
55
|
+
export declare const PINNED_BACKUP_DIRNAME = "pinned";
|
|
56
|
+
export declare const DATABASE_BACKUP_SKIPPED_MARKER = "backup-skipped.json";
|
|
57
|
+
export declare class DatabaseBackupSkippedError extends Error {
|
|
58
|
+
readonly backupDir: string;
|
|
59
|
+
readonly freeBytes: number;
|
|
60
|
+
readonly requiredBytes: number;
|
|
61
|
+
readonly code = "database_backup_skipped_low_disk";
|
|
62
|
+
constructor(backupDir: string, freeBytes: number, requiredBytes: number);
|
|
63
|
+
}
|
|
64
|
+
export declare function readBackupDiskSpace(dir: string): BackupDiskSpace;
|
|
65
|
+
export declare function pinnedBackupDir(backupDir: string): string;
|
|
66
|
+
/** Decompresses a gzip backup end to end; rejects empty, truncated or corrupt archives. */
|
|
67
|
+
export declare function verifyGzipBackup(file: string): Promise<void>;
|
|
68
|
+
/** Deletes partial backups left by a killed backup process; returns the number removed. */
|
|
69
|
+
export declare function removeAbandonedPartialBackups(backupDir: string, nowMs?: number): number;
|
|
70
|
+
/** Renames backups written under the retired filename prefix so retention keeps pruning them; idempotent. */
|
|
71
|
+
export declare function adoptRetiredBackupFilenames(backupDir: string): number;
|
|
34
72
|
export declare function createBufferedTextFileWriter(filePath: string, maxBufferedBytes?: number): {
|
|
35
73
|
emit(line: string): void;
|
|
36
74
|
drain(): Promise<void>;
|
package/dist/backup-lib.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backup-lib.d.ts","sourceRoot":"","sources":["../src/backup-lib.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"backup-lib.d.ts","sourceRoot":"","sources":["../src/backup-lib.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,wGAAwG;IACxG,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,qBAAqB,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1C,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;IACjD,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,2FAA2F;IAC3F,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kGAAkG;IAClG,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,+EAA+E;IAC/E,UAAU,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AA0EF,eAAO,MAAM,+BAA+B,aAAa,CAAC;AAC1D,eAAO,MAAM,qBAAqB,WAAW,CAAC;AAC9C,eAAO,MAAM,8BAA8B,wBAAwB,CAAC;AAIpE,qBAAa,0BAA2B,SAAQ,KAAK;IAIjD,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM;IALhC,QAAQ,CAAC,IAAI,sCAAsC;IAEnD,YACW,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EAM/B;CACF;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAGhE;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEzD;AASD,2FAA2F;AAC3F,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOlE;AA2JD,2FAA2F;AAC3F,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,SAAa,GAAG,MAAM,CAiB3F;AAED,6GAA6G;AAC7G,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAWrE;AA2UD,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,EAAE,gBAAgB,SAAoC;IA8B/G,IAAI,OAAO,MAAM;IAUX,KAAK;IAOL,QAAQ,QAAQ,MAAM,GAAG,MAAM;IAS/B,KAAK;IAQL,KAAK;EAgBd;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA4kBxG;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCvF;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,uBAAuB,GAAG,MAAM,CAOlF"}
|
package/dist/backup-lib.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { basename, resolve } from "node:path";
|
|
1
|
+
import { createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statfsSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { open as openFile } from "node:fs/promises";
|
|
6
|
+
import { Writable } from "node:stream";
|
|
6
7
|
import { pipeline } from "node:stream/promises";
|
|
7
8
|
import { createGunzip, createGzip } from "node:zlib";
|
|
8
9
|
import postgres from "postgres";
|
|
@@ -47,6 +48,225 @@ function monthlyRetentionCutoff(nowMs, monthlyMonths) {
|
|
|
47
48
|
const now = new Date(nowMs);
|
|
48
49
|
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - months, 1);
|
|
49
50
|
}
|
|
51
|
+
export const DATABASE_BACKUP_FILENAME_PREFIX = "wardmote";
|
|
52
|
+
export const PINNED_BACKUP_DIRNAME = "pinned";
|
|
53
|
+
export const DATABASE_BACKUP_SKIPPED_MARKER = "backup-skipped.json";
|
|
54
|
+
const DEFAULT_BACKUP_HEADROOM_FACTOR = 1.5;
|
|
55
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
56
|
+
export class DatabaseBackupSkippedError extends Error {
|
|
57
|
+
backupDir;
|
|
58
|
+
freeBytes;
|
|
59
|
+
requiredBytes;
|
|
60
|
+
code = "database_backup_skipped_low_disk";
|
|
61
|
+
constructor(backupDir, freeBytes, requiredBytes) {
|
|
62
|
+
super(`Database backup skipped: ${formatBackupSize(freeBytes)} free on the backup volume, ${formatBackupSize(requiredBytes)} required (last backup x${DEFAULT_BACKUP_HEADROOM_FACTOR} plus reserve).`);
|
|
63
|
+
this.backupDir = backupDir;
|
|
64
|
+
this.freeBytes = freeBytes;
|
|
65
|
+
this.requiredBytes = requiredBytes;
|
|
66
|
+
this.name = "DatabaseBackupSkippedError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function readBackupDiskSpace(dir) {
|
|
70
|
+
const stats = statfsSync(dir);
|
|
71
|
+
return { freeBytes: stats.bavail * stats.bsize, totalBytes: stats.blocks * stats.bsize };
|
|
72
|
+
}
|
|
73
|
+
export function pinnedBackupDir(backupDir) {
|
|
74
|
+
return join(backupDir, PINNED_BACKUP_DIRNAME);
|
|
75
|
+
}
|
|
76
|
+
const verifiedBackups = new Map();
|
|
77
|
+
function backupIdentity(file) {
|
|
78
|
+
const stats = statSync(file);
|
|
79
|
+
return `${stats.size}:${stats.mtimeMs}`;
|
|
80
|
+
}
|
|
81
|
+
/** Decompresses a gzip backup end to end; rejects empty, truncated or corrupt archives. */
|
|
82
|
+
export async function verifyGzipBackup(file) {
|
|
83
|
+
if (statSync(file).size === 0)
|
|
84
|
+
throw new Error(`Backup ${basename(file)} is empty`);
|
|
85
|
+
await pipeline(createReadStream(file, { highWaterMark: 1024 * 1024 }), createGunzip({ chunkSize: 1024 * 1024 }), new Writable({ write(_chunk, _encoding, callback) { callback(); } }));
|
|
86
|
+
}
|
|
87
|
+
function isArchiveCorruption(error) {
|
|
88
|
+
const code = error?.code;
|
|
89
|
+
return typeof code === "string" && code.startsWith("Z_");
|
|
90
|
+
}
|
|
91
|
+
async function checkBackup(file) {
|
|
92
|
+
const stats = statSync(file, { throwIfNoEntry: false });
|
|
93
|
+
if (!stats || stats.size === 0)
|
|
94
|
+
return "invalid";
|
|
95
|
+
if (!file.endsWith(".gz"))
|
|
96
|
+
return "valid";
|
|
97
|
+
if (verifiedBackups.get(file) === `${stats.size}:${stats.mtimeMs}`)
|
|
98
|
+
return "valid";
|
|
99
|
+
try {
|
|
100
|
+
await verifyGzipBackup(file);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return isArchiveCorruption(error) ? "invalid" : "unreadable";
|
|
104
|
+
}
|
|
105
|
+
verifiedBackups.set(file, backupIdentity(file));
|
|
106
|
+
return "valid";
|
|
107
|
+
}
|
|
108
|
+
function listRotationBackups(backupDir, filenamePrefix) {
|
|
109
|
+
if (!existsSync(backupDir))
|
|
110
|
+
return [];
|
|
111
|
+
const entries = [];
|
|
112
|
+
for (const name of readdirSync(backupDir)) {
|
|
113
|
+
if (!name.startsWith(`${filenamePrefix}-`) || !isBackupFilename(name))
|
|
114
|
+
continue;
|
|
115
|
+
const fullPath = resolve(backupDir, name);
|
|
116
|
+
const stats = statSync(fullPath, { throwIfNoEntry: false });
|
|
117
|
+
if (!stats?.isFile())
|
|
118
|
+
continue;
|
|
119
|
+
entries.push({ fullPath, sizeBytes: stats.size, mtimeMs: stats.mtimeMs });
|
|
120
|
+
}
|
|
121
|
+
return entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
122
|
+
}
|
|
123
|
+
function removeBackupFile(file) {
|
|
124
|
+
rmSync(file, { force: true });
|
|
125
|
+
verifiedBackups.delete(file);
|
|
126
|
+
}
|
|
127
|
+
async function pruneToMaxFiles(backupDir, filenamePrefix, maxFiles, newestFile) {
|
|
128
|
+
let kept = 1;
|
|
129
|
+
let removed = 0;
|
|
130
|
+
const unreadable = [];
|
|
131
|
+
for (const entry of listRotationBackups(backupDir, filenamePrefix)) {
|
|
132
|
+
if (entry.fullPath === newestFile)
|
|
133
|
+
continue;
|
|
134
|
+
const check = await checkBackup(entry.fullPath);
|
|
135
|
+
if (check === "unreadable") {
|
|
136
|
+
unreadable.push(entry.fullPath);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (kept < maxFiles && check === "valid") {
|
|
140
|
+
kept += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
removeBackupFile(entry.fullPath);
|
|
144
|
+
removed += 1;
|
|
145
|
+
}
|
|
146
|
+
return { removed, unreadable };
|
|
147
|
+
}
|
|
148
|
+
function prunePinnedBackups(backupDir, maxAgeDays, nowMs = Date.now()) {
|
|
149
|
+
if (!maxAgeDays || maxAgeDays <= 0)
|
|
150
|
+
return 0;
|
|
151
|
+
const dir = pinnedBackupDir(backupDir);
|
|
152
|
+
if (!existsSync(dir))
|
|
153
|
+
return 0;
|
|
154
|
+
let removed = 0;
|
|
155
|
+
for (const name of readdirSync(dir)) {
|
|
156
|
+
if (!isBackupFilename(name))
|
|
157
|
+
continue;
|
|
158
|
+
const fullPath = resolve(dir, name);
|
|
159
|
+
const stats = statSync(fullPath, { throwIfNoEntry: false });
|
|
160
|
+
if (!stats?.isFile() || nowMs - stats.mtimeMs < maxAgeDays * DAY_MS)
|
|
161
|
+
continue;
|
|
162
|
+
removeBackupFile(fullPath);
|
|
163
|
+
removed += 1;
|
|
164
|
+
}
|
|
165
|
+
return removed;
|
|
166
|
+
}
|
|
167
|
+
function backupReserveBytes(guard, totalBytes) {
|
|
168
|
+
return Math.max(guard.reserveBytes, Math.ceil((totalBytes * guard.reservePercent) / 100));
|
|
169
|
+
}
|
|
170
|
+
/** Frees room for one more backup by dropping the oldest rotated files, never the newest valid one; otherwise records a skip and throws. */
|
|
171
|
+
async function ensureBackupSpace(input) {
|
|
172
|
+
const { guard } = input;
|
|
173
|
+
const statfs = guard.statfs ?? readBackupDiskSpace;
|
|
174
|
+
const existing = listRotationBackups(input.backupDir, input.filenamePrefix);
|
|
175
|
+
const lastSize = existing.find((entry) => entry.sizeBytes > 0)?.sizeBytes ?? 0;
|
|
176
|
+
const space = statfs(input.targetDir);
|
|
177
|
+
const requiredBytes = Math.ceil(lastSize * (guard.headroomFactor ?? DEFAULT_BACKUP_HEADROOM_FACTOR)) +
|
|
178
|
+
backupReserveBytes(guard, space.totalBytes);
|
|
179
|
+
if (space.freeBytes >= requiredBytes)
|
|
180
|
+
return 0;
|
|
181
|
+
if (input.rotate) {
|
|
182
|
+
const invalid = [];
|
|
183
|
+
const valid = [];
|
|
184
|
+
for (const entry of existing) {
|
|
185
|
+
const check = await checkBackup(entry.fullPath);
|
|
186
|
+
if (check === "valid")
|
|
187
|
+
valid.push(entry);
|
|
188
|
+
else if (check === "invalid")
|
|
189
|
+
invalid.push(entry);
|
|
190
|
+
}
|
|
191
|
+
const chosen = [];
|
|
192
|
+
let reclaimed = 0;
|
|
193
|
+
for (const entry of [...invalid, ...valid.slice(1).reverse()]) {
|
|
194
|
+
if (space.freeBytes + reclaimed >= requiredBytes)
|
|
195
|
+
break;
|
|
196
|
+
chosen.push(entry);
|
|
197
|
+
reclaimed += entry.sizeBytes;
|
|
198
|
+
}
|
|
199
|
+
if (space.freeBytes + reclaimed >= requiredBytes) {
|
|
200
|
+
for (const entry of chosen)
|
|
201
|
+
removeBackupFile(entry.fullPath);
|
|
202
|
+
return chosen.length;
|
|
203
|
+
}
|
|
204
|
+
writeFileSync(join(input.backupDir, DATABASE_BACKUP_SKIPPED_MARKER), JSON.stringify({
|
|
205
|
+
skippedAt: new Date().toISOString(),
|
|
206
|
+
freeBytes: space.freeBytes,
|
|
207
|
+
requiredBytes,
|
|
208
|
+
totalBytes: space.totalBytes,
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
throw new DatabaseBackupSkippedError(input.backupDir, space.freeBytes, requiredBytes);
|
|
212
|
+
}
|
|
213
|
+
const RETIRED_BACKUP_FILENAME_PREFIX = "paperclip";
|
|
214
|
+
function isBackupFilename(name) {
|
|
215
|
+
return name.endsWith(".sql") || name.endsWith(".sql.gz");
|
|
216
|
+
}
|
|
217
|
+
const PARTIAL_BACKUP_PATTERN = /\.(\d+)\.partial$/;
|
|
218
|
+
const ABANDONED_PARTIAL_BACKUP_AGE_MS = 24 * 60 * 60 * 1000;
|
|
219
|
+
const activePartialBackupFiles = new Set();
|
|
220
|
+
function partialBackupPath(finalPath) {
|
|
221
|
+
return `${finalPath}.${process.pid}.partial`;
|
|
222
|
+
}
|
|
223
|
+
function isProcessAlive(pid) {
|
|
224
|
+
try {
|
|
225
|
+
process.kill(pid, 0);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
return error.code === "EPERM";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Deletes partial backups left by a killed backup process; returns the number removed. */
|
|
233
|
+
export function removeAbandonedPartialBackups(backupDir, nowMs = Date.now()) {
|
|
234
|
+
if (!existsSync(backupDir))
|
|
235
|
+
return 0;
|
|
236
|
+
let removed = 0;
|
|
237
|
+
for (const name of readdirSync(backupDir)) {
|
|
238
|
+
const match = PARTIAL_BACKUP_PATTERN.exec(name);
|
|
239
|
+
if (!match || !isBackupFilename(name.slice(0, match.index)))
|
|
240
|
+
continue;
|
|
241
|
+
const fullPath = resolve(backupDir, name);
|
|
242
|
+
const pid = Number(match[1]);
|
|
243
|
+
const ownerGone = pid === process.pid ? !activePartialBackupFiles.has(fullPath) : !isProcessAlive(pid);
|
|
244
|
+
if (!ownerGone) {
|
|
245
|
+
const mtimeMs = statSync(fullPath, { throwIfNoEntry: false })?.mtimeMs;
|
|
246
|
+
if (mtimeMs === undefined || nowMs - mtimeMs < ABANDONED_PARTIAL_BACKUP_AGE_MS)
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
rmSync(fullPath, { force: true });
|
|
250
|
+
removed += 1;
|
|
251
|
+
}
|
|
252
|
+
return removed;
|
|
253
|
+
}
|
|
254
|
+
/** Renames backups written under the retired filename prefix so retention keeps pruning them; idempotent. */
|
|
255
|
+
export function adoptRetiredBackupFilenames(backupDir) {
|
|
256
|
+
if (!existsSync(backupDir))
|
|
257
|
+
return 0;
|
|
258
|
+
let renamed = 0;
|
|
259
|
+
for (const name of readdirSync(backupDir)) {
|
|
260
|
+
if (!name.startsWith(`${RETIRED_BACKUP_FILENAME_PREFIX}-`) || !isBackupFilename(name))
|
|
261
|
+
continue;
|
|
262
|
+
const next = resolve(backupDir, `${DATABASE_BACKUP_FILENAME_PREFIX}${name.slice(RETIRED_BACKUP_FILENAME_PREFIX.length)}`);
|
|
263
|
+
if (existsSync(next))
|
|
264
|
+
continue;
|
|
265
|
+
renameSync(resolve(backupDir, name), next);
|
|
266
|
+
renamed += 1;
|
|
267
|
+
}
|
|
268
|
+
return renamed;
|
|
269
|
+
}
|
|
50
270
|
/**
|
|
51
271
|
* Tiered backup pruning:
|
|
52
272
|
* - Daily tier: keep ALL backups from the last `dailyDays` days
|
|
@@ -65,7 +285,7 @@ function pruneOldBackups(backupDir, retention, filenamePrefix) {
|
|
|
65
285
|
for (const name of readdirSync(backupDir)) {
|
|
66
286
|
if (!name.startsWith(`${filenamePrefix}-`))
|
|
67
287
|
continue;
|
|
68
|
-
if (!
|
|
288
|
+
if (!isBackupFilename(name))
|
|
69
289
|
continue;
|
|
70
290
|
const fullPath = resolve(backupDir, name);
|
|
71
291
|
const stat = statSync(fullPath);
|
|
@@ -419,7 +639,7 @@ export function createBufferedTextFileWriter(filePath, maxBufferedBytes = DEFAUL
|
|
|
419
639
|
};
|
|
420
640
|
}
|
|
421
641
|
export async function runDatabaseBackup(opts) {
|
|
422
|
-
const filenamePrefix = opts.filenamePrefix ??
|
|
642
|
+
const filenamePrefix = opts.filenamePrefix ?? DATABASE_BACKUP_FILENAME_PREFIX;
|
|
423
643
|
const retention = opts.retention;
|
|
424
644
|
const connectTimeout = Math.max(1, Math.trunc(opts.connectTimeoutSeconds ?? 5));
|
|
425
645
|
const backupEngine = opts.backupEngine ?? "auto";
|
|
@@ -435,13 +655,57 @@ export async function runDatabaseBackup(opts) {
|
|
|
435
655
|
sqlClosed = true;
|
|
436
656
|
await sql.end();
|
|
437
657
|
};
|
|
438
|
-
|
|
658
|
+
const targetDir = opts.pinned ? pinnedBackupDir(opts.backupDir) : opts.backupDir;
|
|
659
|
+
mkdirSync(targetDir, { recursive: true });
|
|
660
|
+
if (filenamePrefix === DATABASE_BACKUP_FILENAME_PREFIX)
|
|
661
|
+
adoptRetiredBackupFilenames(opts.backupDir);
|
|
439
662
|
// Initial and final sleep checks can run within the same second. A failed
|
|
440
663
|
// second checkpoint must never overwrite the first verified recovery point.
|
|
441
664
|
const checkpointSuffix = opts.verifyBeforePrune ? `-${randomUUID()}` : "";
|
|
442
|
-
|
|
443
|
-
|
|
665
|
+
removeAbandonedPartialBackups(targetDir);
|
|
666
|
+
let freedForSpace = 0;
|
|
667
|
+
if (opts.spaceGuard) {
|
|
668
|
+
try {
|
|
669
|
+
freedForSpace = await ensureBackupSpace({
|
|
670
|
+
backupDir: opts.backupDir,
|
|
671
|
+
targetDir,
|
|
672
|
+
filenamePrefix,
|
|
673
|
+
guard: opts.spaceGuard,
|
|
674
|
+
rotate: !opts.pinned,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
await closeSql();
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const finalSqlFile = resolve(targetDir, `${filenamePrefix}-${timestamp()}${checkpointSuffix}.sql`);
|
|
683
|
+
const backupFile = `${finalSqlFile}.gz`;
|
|
684
|
+
const sqlFile = partialBackupPath(finalSqlFile);
|
|
685
|
+
const partialBackupFile = partialBackupPath(backupFile);
|
|
686
|
+
activePartialBackupFiles.add(sqlFile);
|
|
687
|
+
activePartialBackupFiles.add(partialBackupFile);
|
|
444
688
|
const writer = createBufferedTextFileWriter(sqlFile);
|
|
689
|
+
const finalizeBackup = async () => {
|
|
690
|
+
await verifyGzipBackup(partialBackupFile);
|
|
691
|
+
renameSync(partialBackupFile, backupFile);
|
|
692
|
+
verifiedBackups.set(backupFile, backupIdentity(backupFile));
|
|
693
|
+
const sizeBytes = statSync(backupFile).size;
|
|
694
|
+
await opts.verifyBeforePrune?.(backupFile);
|
|
695
|
+
if (opts.pinned)
|
|
696
|
+
return { backupFile, sizeBytes, prunedCount: freedForSpace };
|
|
697
|
+
const rotated = retention.maxFiles && retention.maxFiles > 0
|
|
698
|
+
? await pruneToMaxFiles(opts.backupDir, filenamePrefix, retention.maxFiles, backupFile)
|
|
699
|
+
: { removed: pruneOldBackups(opts.backupDir, retention, filenamePrefix), unreadable: [] };
|
|
700
|
+
const prunedCount = freedForSpace + rotated.removed + prunePinnedBackups(opts.backupDir, opts.pinnedMaxAgeDays);
|
|
701
|
+
rmSync(join(opts.backupDir, DATABASE_BACKUP_SKIPPED_MARKER), { force: true });
|
|
702
|
+
return {
|
|
703
|
+
backupFile,
|
|
704
|
+
sizeBytes,
|
|
705
|
+
prunedCount,
|
|
706
|
+
...(rotated.unreadable.length > 0 ? { unreadableBackups: rotated.unreadable } : {}),
|
|
707
|
+
};
|
|
708
|
+
};
|
|
445
709
|
try {
|
|
446
710
|
if (backupEngine === "pg_dump" || (backupEngine === "auto" && canUsePgDump)) {
|
|
447
711
|
await sql `SELECT 1`;
|
|
@@ -450,15 +714,15 @@ export async function runDatabaseBackup(opts) {
|
|
|
450
714
|
await closeSql();
|
|
451
715
|
await runPgDumpBackup({
|
|
452
716
|
connectionString: opts.connectionString,
|
|
453
|
-
backupFile,
|
|
717
|
+
backupFile: partialBackupFile,
|
|
454
718
|
connectTimeout,
|
|
455
719
|
});
|
|
456
720
|
dumped = true;
|
|
457
721
|
}
|
|
458
722
|
catch (error) {
|
|
459
|
-
if (existsSync(
|
|
723
|
+
if (existsSync(partialBackupFile)) {
|
|
460
724
|
try {
|
|
461
|
-
unlinkSync(
|
|
725
|
+
unlinkSync(partialBackupFile);
|
|
462
726
|
}
|
|
463
727
|
catch { /* ignore */ }
|
|
464
728
|
}
|
|
@@ -474,10 +738,7 @@ export async function runDatabaseBackup(opts) {
|
|
|
474
738
|
// pg_dump succeeds, archive verification or persistence failures
|
|
475
739
|
// must retain their original error and must not reuse a closed writer.
|
|
476
740
|
await writer.abort();
|
|
477
|
-
|
|
478
|
-
await opts.verifyBeforePrune?.(backupFile);
|
|
479
|
-
const prunedCount = pruneOldBackups(opts.backupDir, retention, filenamePrefix);
|
|
480
|
-
return { backupFile, sizeBytes, prunedCount };
|
|
741
|
+
return await finalizeBackup();
|
|
481
742
|
}
|
|
482
743
|
}
|
|
483
744
|
await sql `SELECT 1`;
|
|
@@ -489,7 +750,7 @@ export async function runDatabaseBackup(opts) {
|
|
|
489
750
|
const emitStatementBoundary = () => {
|
|
490
751
|
emit(STATEMENT_BREAKPOINT);
|
|
491
752
|
};
|
|
492
|
-
emit("--
|
|
753
|
+
emit("-- Wardmote database backup");
|
|
493
754
|
emit(`-- Created: ${new Date().toISOString()}`);
|
|
494
755
|
emit("");
|
|
495
756
|
emitStatement("BEGIN;");
|
|
@@ -884,35 +1145,26 @@ export async function runDatabaseBackup(opts) {
|
|
|
884
1145
|
await writer.close();
|
|
885
1146
|
// Compress the SQL file with gzip
|
|
886
1147
|
const sqlReadStream = createReadStream(sqlFile);
|
|
887
|
-
const gzWriteStream = createWriteStream(
|
|
1148
|
+
const gzWriteStream = createWriteStream(partialBackupFile);
|
|
888
1149
|
await pipeline(sqlReadStream, createGzip(), gzWriteStream);
|
|
889
1150
|
unlinkSync(sqlFile);
|
|
890
|
-
|
|
891
|
-
await opts.verifyBeforePrune?.(backupFile);
|
|
892
|
-
const prunedCount = pruneOldBackups(opts.backupDir, retention, filenamePrefix);
|
|
893
|
-
return {
|
|
894
|
-
backupFile,
|
|
895
|
-
sizeBytes,
|
|
896
|
-
prunedCount,
|
|
897
|
-
};
|
|
1151
|
+
return await finalizeBackup();
|
|
898
1152
|
}
|
|
899
1153
|
catch (error) {
|
|
900
1154
|
await writer.abort();
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if (existsSync(sqlFile)) {
|
|
908
|
-
try {
|
|
909
|
-
unlinkSync(sqlFile);
|
|
1155
|
+
for (const file of [backupFile, partialBackupFile, sqlFile]) {
|
|
1156
|
+
if (existsSync(file)) {
|
|
1157
|
+
try {
|
|
1158
|
+
unlinkSync(file);
|
|
1159
|
+
}
|
|
1160
|
+
catch { /* ignore */ }
|
|
910
1161
|
}
|
|
911
|
-
catch { /* ignore */ }
|
|
912
1162
|
}
|
|
913
1163
|
throw error;
|
|
914
1164
|
}
|
|
915
1165
|
finally {
|
|
1166
|
+
activePartialBackupFiles.delete(sqlFile);
|
|
1167
|
+
activePartialBackupFiles.delete(partialBackupFile);
|
|
916
1168
|
await closeSql();
|
|
917
1169
|
}
|
|
918
1170
|
}
|
|
@@ -953,6 +1205,9 @@ export async function runDatabaseRestore(opts) {
|
|
|
953
1205
|
export function formatDatabaseBackupResult(result) {
|
|
954
1206
|
const size = formatBackupSize(result.sizeBytes);
|
|
955
1207
|
const pruned = result.prunedCount > 0 ? `; pruned ${result.prunedCount} old backup(s)` : "";
|
|
956
|
-
|
|
1208
|
+
const unreadable = result.unreadableBackups?.length
|
|
1209
|
+
? `; kept ${result.unreadableBackups.length} unreadable backup(s)`
|
|
1210
|
+
: "";
|
|
1211
|
+
return `${result.backupFile} (${size}${pruned}${unreadable})`;
|
|
957
1212
|
}
|
|
958
1213
|
//# sourceMappingURL=backup-lib.js.map
|