signalk-backup 0.3.0 → 0.5.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 (31) hide show
  1. package/README.md +2 -0
  2. package/package.json +3 -3
  3. package/plugin/config/image-tag.d.ts +3 -0
  4. package/plugin/config/image-tag.d.ts.map +1 -0
  5. package/plugin/config/image-tag.js +16 -0
  6. package/plugin/config/image-tag.js.map +1 -0
  7. package/plugin/config/schema.d.ts +2 -0
  8. package/plugin/config/schema.d.ts.map +1 -1
  9. package/plugin/config/schema.js +16 -4
  10. package/plugin/config/schema.js.map +1 -1
  11. package/plugin/database-export/grafana.d.ts +25 -0
  12. package/plugin/database-export/grafana.d.ts.map +1 -0
  13. package/plugin/database-export/grafana.js +262 -0
  14. package/plugin/database-export/grafana.js.map +1 -0
  15. package/plugin/database-export/index.d.ts +9 -2
  16. package/plugin/database-export/index.d.ts.map +1 -1
  17. package/plugin/database-export/index.js +24 -6
  18. package/plugin/database-export/index.js.map +1 -1
  19. package/plugin/database-export/signalk-database.d.ts +21 -0
  20. package/plugin/database-export/signalk-database.d.ts.map +1 -0
  21. package/plugin/database-export/signalk-database.js +148 -0
  22. package/plugin/database-export/signalk-database.js.map +1 -0
  23. package/plugin/index.d.ts.map +1 -1
  24. package/plugin/index.js +115 -42
  25. package/plugin/index.js.map +1 -1
  26. package/public/assets/{index-C97Ruecu.js → index-BbZnGesz.js} +2 -2
  27. package/public/assets/index-BbZnGesz.js.map +1 -0
  28. package/public/assets/{main-DtkejeIo.js → main-Bb6YhYnz.js} +2 -2
  29. package/public/assets/{main-DtkejeIo.js.map → main-Bb6YhYnz.js.map} +1 -1
  30. package/public/index.html +1 -1
  31. package/public/assets/index-C97Ruecu.js.map +0 -1
package/README.md CHANGED
@@ -10,6 +10,7 @@ The plugin is a thin shell: it asks [signalk-container](https://github.com/dirkw
10
10
  - **Cloud sync to Google Drive** — optional, opt-in. Uses rclone with `drive.file` scope (the app only sees files it created)
11
11
  - **Scheduled backups** — hourly / daily / weekly / on-startup tiers, each with independent retention
12
12
  - **Restore with safety backup** — every restore creates a snapshot of the current state first, so a botched restore can be rolled back automatically
13
+ - **Database export pipeline** — opt-in exporters for [signalk-questdb](https://github.com/dirkwa/signalk-questdb), [signalk-grafana](https://github.com/dirkwa/signalk-grafana), and [signalk-database](https://github.com/dirkwa/signalk-database) periodically write consistent snapshots (parquet, SQLite checkpoint, provisioning YAML) to a staging dir that kopia includes in every snapshot. Live DB files are excluded from filesystem-level backup to avoid torn-write hazards; the export side is how DB state actually gets backed up. Manual backups run pending exports synchronously first.
13
14
  - **First-run safe default** — installs with a daily local-only schedule already seeded; no surprise data egress
14
15
  - **Update detection** — checks ghcr.io for new container images via signalk-container's centralized update service
15
16
 
@@ -50,6 +51,7 @@ Most configuration lives in the **Backup Console** (open it from the redirect, o
50
51
  - Repository password (Kopia encryption key)
51
52
  - Cloud sync mode (off / manual / daily / weekly)
52
53
  - Google Drive connection (one-click OAuth via `rclone authorize drive`)
54
+ - Database export toggles (QuestDB, Grafana, signalk-database) and export interval
53
55
  - Snapshot browser, restore wizard
54
56
 
55
57
  The SignalK Admin UI plugin panel (small) handles only deployment-level settings:
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "signalk-backup",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scheduled deduplicated backups of SignalK config to local storage and Google Drive (via Kopia + rclone)",
5
5
  "main": "plugin/index.js",
6
6
  "type": "module",
7
7
  "signalk-plugin-enabled-by-default": true,
8
8
  "scripts": {
9
- "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts' 'webapp/**/*.{ts,tsx,html}' && eslint --fix 'src/**/*.ts' 'test/**/*.ts' 'webapp/**/*.{ts,tsx}'",
10
- "lint": "eslint 'src/**/*.ts' 'test/**/*.ts' 'webapp/**/*.{ts,tsx}'",
9
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"webapp/**/*.{ts,tsx,html}\" && eslint --fix \"src/**/*.ts\" \"test/**/*.ts\" \"webapp/**/*.{ts,tsx}\"",
10
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" \"webapp/**/*.{ts,tsx}\"",
11
11
  "typecheck:webapp": "tsc -p tsconfig.webapp.json",
12
12
  "build:plugin": "tsc",
13
13
  "build:webapp": "vite build",
@@ -0,0 +1,3 @@
1
+ export declare const PLUGIN_VERSION: string;
2
+ export declare function resolveImageTag(tag: string): string;
3
+ //# sourceMappingURL=image-tag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-tag.d.ts","sourceRoot":"","sources":["../../src/config/image-tag.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,QAOvB,CAAA;AAGJ,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD"}
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ // CI publishes signalk-backup-server:X.Y.Z in lockstep with this plugin's package.json version.
4
+ export const PLUGIN_VERSION = (() => {
5
+ const pkgPath = fileURLToPath(new URL('../../package.json', import.meta.url));
6
+ const raw = JSON.parse(readFileSync(pkgPath, 'utf-8'));
7
+ if (typeof raw.version !== 'string') {
8
+ throw new Error('signalk-backup package.json has no version field');
9
+ }
10
+ return raw.version;
11
+ })();
12
+ // 'auto' resolves to PLUGIN_VERSION so the container tracks plugin upgrades; other tags pass through.
13
+ export function resolveImageTag(tag) {
14
+ return tag === 'auto' ? PLUGIN_VERSION : tag;
15
+ }
16
+ //# sourceMappingURL=image-tag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-tag.js","sourceRoot":"","sources":["../../src/config/image-tag.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,gGAAgG;AAChG,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE;IAClC,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IAC7E,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAA0B,CAAA;IAC/E,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,GAAG,CAAC,OAAO,CAAA;AACpB,CAAC,CAAC,EAAE,CAAA;AAEJ,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,OAAO,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAA;AAC9C,CAAC"}
@@ -15,6 +15,8 @@ export declare const ConfigSchema: import("@sinclair/typebox").TObject<{
15
15
  externalUrl: import("@sinclair/typebox").TString;
16
16
  databaseExport: import("@sinclair/typebox").TObject<{
17
17
  questdb: import("@sinclair/typebox").TBoolean;
18
+ grafana: import("@sinclair/typebox").TBoolean;
19
+ signalkDatabase: import("@sinclair/typebox").TBoolean;
18
20
  intervalMinutes: import("@sinclair/typebox").TNumber;
19
21
  }>;
20
22
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY;;;;;;;;EA6BvB,CAAA;AAEF,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAA;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAQ7B,CAAA"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;EAwCvB,CAAA;AAEF,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAA;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAU7B,CAAA"}
@@ -17,9 +17,10 @@ export const ConfigSchema = Type.Object({
17
17
  'Disable to point at an external backup-server instance via "External URL".'
18
18
  }),
19
19
  imageTag: Type.String({
20
- default: 'latest',
20
+ default: 'auto',
21
21
  title: 'Container image tag',
22
- description: 'Pin to a specific version (e.g. "0.1.0") or use a floating tag (e.g. "latest").'
22
+ description: '"auto" (default) tracks the plugin version, so upgrading the plugin upgrades the container. ' +
23
+ 'Pin to a specific version (e.g. "0.4.0") or use a floating tag (e.g. "latest") to override.'
23
24
  }),
24
25
  externalUrl: Type.String({
25
26
  default: '',
@@ -31,8 +32,17 @@ export const ConfigSchema = Type.Object({
31
32
  // form hides it via uiSchema in src/index.ts (webapp owns the UI).
32
33
  databaseExport: Type.Object({
33
34
  questdb: Type.Boolean({ default: false }),
35
+ grafana: Type.Boolean({ default: false }),
36
+ signalkDatabase: Type.Boolean({ default: false }),
34
37
  intervalMinutes: Type.Number({ default: 60, minimum: 5, maximum: 1440 })
35
- }, { default: { questdb: false, intervalMinutes: 60 } })
38
+ }, {
39
+ default: {
40
+ questdb: false,
41
+ grafana: false,
42
+ signalkDatabase: false,
43
+ intervalMinutes: 60
44
+ }
45
+ })
36
46
  });
37
47
  /**
38
48
  * Materialised defaults — Signal K only uses the schema's `default` fields
@@ -46,10 +56,12 @@ export const ConfigSchema = Type.Object({
46
56
  */
47
57
  export const SCHEMA_DEFAULTS = {
48
58
  managedContainer: true,
49
- imageTag: 'latest',
59
+ imageTag: 'auto',
50
60
  externalUrl: '',
51
61
  databaseExport: {
52
62
  questdb: false,
63
+ grafana: false,
64
+ signalkDatabase: false,
53
65
  intervalMinutes: 60
54
66
  }
55
67
  };
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAU,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;IACtC,gBAAgB,EAAE,IAAI,CAAC,OAAO,CAAC;QAC7B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,+CAA+C;QACtD,WAAW,EACT,0FAA0F;YAC1F,4EAA4E;KAC/E,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,iFAAiF;KAC/F,CAAC;IACF,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;QACvB,OAAO,EAAE,EAAE;QACX,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,8EAA8E;YAC9E,0CAA0C;KAC7C,CAAC;IACF,oEAAoE;IACpE,mEAAmE;IACnE,cAAc,EAAE,IAAI,CAAC,MAAM,CACzB;QACE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACzC,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;KACzE,EACD,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,EAAE,EAAE,CACrD;CACF,CAAC,CAAA;AAIF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAW;IACrC,gBAAgB,EAAE,IAAI;IACtB,QAAQ,EAAE,QAAQ;IAClB,WAAW,EAAE,EAAE;IACf,cAAc,EAAE;QACd,OAAO,EAAE,KAAK;QACd,eAAe,EAAE,EAAE;KACpB;CACF,CAAA"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAU,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;IACtC,gBAAgB,EAAE,IAAI,CAAC,OAAO,CAAC;QAC7B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,+CAA+C;QACtD,WAAW,EACT,0FAA0F;YAC1F,4EAA4E;KAC/E,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EACT,8FAA8F;YAC9F,6FAA6F;KAChG,CAAC;IACF,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;QACvB,OAAO,EAAE,EAAE;QACX,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,8EAA8E;YAC9E,0CAA0C;KAC7C,CAAC;IACF,oEAAoE;IACpE,mEAAmE;IACnE,cAAc,EAAE,IAAI,CAAC,MAAM,CACzB;QACE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACzC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACzC,eAAe,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACjD,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;KACzE,EACD;QACE,OAAO,EAAE;YACP,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,KAAK;YACd,eAAe,EAAE,KAAK;YACtB,eAAe,EAAE,EAAE;SACpB;KACF,CACF;CACF,CAAC,CAAA;AAIF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAW;IACrC,gBAAgB,EAAE,IAAI;IACtB,QAAQ,EAAE,MAAM;IAChB,WAAW,EAAE,EAAE;IACf,cAAc,EAAE;QACd,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,KAAK;QACd,eAAe,EAAE,KAAK;QACtB,eAAe,EAAE,EAAE;KACpB;CACF,CAAA"}
@@ -0,0 +1,25 @@
1
+ import type { DatabaseExporter, ExportResult } from './types.js';
2
+ export interface GrafanaExporterOptions {
3
+ signalkBaseUrl?: string;
4
+ log?: (msg: string) => void;
5
+ /** Override fetch (tests). */
6
+ fetch?: typeof fetch;
7
+ }
8
+ export declare class GrafanaExporter implements DatabaseExporter {
9
+ readonly pluginId = "signalk-grafana";
10
+ private readonly baseUrl;
11
+ private readonly fetchImpl;
12
+ private readonly log;
13
+ constructor(opts?: GrafanaExporterOptions);
14
+ detect(): Promise<boolean>;
15
+ exportAll(stagingDir: string): Promise<ExportResult>;
16
+ private exportDb;
17
+ private exportDashboards;
18
+ private parseDashboardEntry;
19
+ private fetchDashboardFile;
20
+ private exportProvisioning;
21
+ private parseProvisioningEntry;
22
+ private fetchProvisioningFile;
23
+ private resetStagingDir;
24
+ }
25
+ //# sourceMappingURL=grafana.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grafana.d.ts","sourceRoot":"","sources":["../../src/database-export/grafana.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAe,MAAM,YAAY,CAAA;AAS7E,MAAM,WAAW,sBAAsB;IACrC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3B,8BAA8B;IAC9B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACrB;AAeD,qBAAa,eAAgB,YAAW,gBAAgB;IACtD,QAAQ,CAAC,QAAQ,qBAAoB;IAErC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAuB;gBAE/B,IAAI,GAAE,sBAA2B;IAMvC,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAc1B,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;YA8D5C,QAAQ;YAkBR,gBAAgB;IAuC9B,OAAO,CAAC,mBAAmB;YAYb,kBAAkB;YASlB,kBAAkB;IAuChC,OAAO,CAAC,sBAAsB;YAiBhB,qBAAqB;YAerB,eAAe;CAI9B"}
@@ -0,0 +1,262 @@
1
+ // Mirrors the QuestDB exporter: pulls signalk-grafana's /api/full-export/{db,dashboards,provisioning} over loopback and stages into kopia's snapshot tree.
2
+ import { mkdir, rename, rm, unlink } from 'node:fs/promises';
3
+ import { createWriteStream, existsSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { Readable } from 'node:stream';
7
+ const GRAFANA_PLUGIN_ID = 'signalk-grafana';
8
+ const DEFAULT_SIGNALK_BASE = 'http://127.0.0.1:3000';
9
+ const FETCH_TIMEOUT_MS = 600_000;
10
+ // Engine-side validation rejects names outside this set; reject early
11
+ // here too so we never even try a fetch that the server would 400.
12
+ const SAFE_FILENAME = /^[a-zA-Z0-9._-]+$/;
13
+ export class GrafanaExporter {
14
+ pluginId = GRAFANA_PLUGIN_ID;
15
+ baseUrl;
16
+ fetchImpl;
17
+ log;
18
+ constructor(opts = {}) {
19
+ this.baseUrl = (opts.signalkBaseUrl ?? DEFAULT_SIGNALK_BASE).replace(/\/$/, '');
20
+ this.fetchImpl = opts.fetch ?? fetch;
21
+ this.log = (msg) => opts.log?.(`[grafana-export] ${msg}`);
22
+ }
23
+ async detect() {
24
+ // Dashboards manifest is the cheapest probe — GET, no container exec, 200 even when /var/lib/grafana is empty.
25
+ try {
26
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/dashboards`;
27
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(5_000) });
28
+ if (!res.ok)
29
+ return false;
30
+ const body = (await res.json());
31
+ return Array.isArray(body.dashboards);
32
+ }
33
+ catch (err) {
34
+ this.log(`detect failed: ${errMsg(err)}`);
35
+ return false;
36
+ }
37
+ }
38
+ async exportAll(stagingDir) {
39
+ const startedAt = Date.now();
40
+ await mkdir(stagingDir, { recursive: true });
41
+ const exports = [];
42
+ let totalBytes = 0;
43
+ // Use TableExport-as-single-shard so totals line up with QuestDB reports.
44
+ try {
45
+ const dbBytes = await this.exportDb(stagingDir);
46
+ exports.push({
47
+ table: 'grafana.db',
48
+ tableDir: stagingDir,
49
+ shardsWritten: 1,
50
+ shardsSkipped: 0,
51
+ bytes: dbBytes
52
+ });
53
+ totalBytes += dbBytes;
54
+ }
55
+ catch (err) {
56
+ this.log(`db export failed: ${errMsg(err)}`);
57
+ }
58
+ try {
59
+ const bytes = await this.exportDashboards(stagingDir);
60
+ exports.push({
61
+ table: 'dashboards',
62
+ tableDir: join(stagingDir, 'dashboards'),
63
+ shardsWritten: bytes.count,
64
+ shardsSkipped: bytes.skipped,
65
+ bytes: bytes.totalBytes
66
+ });
67
+ totalBytes += bytes.totalBytes;
68
+ }
69
+ catch (err) {
70
+ this.log(`dashboards export failed: ${errMsg(err)}`);
71
+ }
72
+ try {
73
+ const bytes = await this.exportProvisioning(stagingDir);
74
+ exports.push({
75
+ table: 'provisioning',
76
+ tableDir: join(stagingDir, 'provisioning'),
77
+ shardsWritten: bytes.count,
78
+ shardsSkipped: bytes.skipped,
79
+ bytes: bytes.totalBytes
80
+ });
81
+ totalBytes += bytes.totalBytes;
82
+ }
83
+ catch (err) {
84
+ this.log(`provisioning export failed: ${errMsg(err)}`);
85
+ }
86
+ return {
87
+ pluginId: this.pluginId,
88
+ tables: exports,
89
+ totalBytes,
90
+ durationMs: Date.now() - startedAt
91
+ };
92
+ }
93
+ // -------------------------------------------------------------------
94
+ // Internal
95
+ // -------------------------------------------------------------------
96
+ async exportDb(stagingDir) {
97
+ const finalPath = join(stagingDir, 'grafana.db');
98
+ // Drop the prior-cycle file first so a mid-cycle failure leaves no stale grafana.db beside fresh dashboards.
99
+ if (existsSync(finalPath))
100
+ await unlink(finalPath).catch(() => undefined);
101
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/db`;
102
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
103
+ if (!res.ok) {
104
+ throw new Error(`db HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
105
+ }
106
+ if (!res.body) {
107
+ throw new Error('db response body is empty');
108
+ }
109
+ const bytes = await atomicWrite(res.body, finalPath);
110
+ this.log(`wrote grafana.db (${bytes} bytes)`);
111
+ return bytes;
112
+ }
113
+ async exportDashboards(stagingDir) {
114
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/dashboards`;
115
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(10_000) });
116
+ if (!res.ok) {
117
+ throw new Error(`dashboards manifest HTTP ${res.status}`);
118
+ }
119
+ const body = (await res.json());
120
+ if (!Array.isArray(body.dashboards)) {
121
+ throw new Error(`dashboards manifest missing 'dashboards' array`);
122
+ }
123
+ const dashboardsDir = join(stagingDir, 'dashboards');
124
+ // Reset so dashboards deleted in Grafana don't linger in kopia forever.
125
+ await this.resetStagingDir(dashboardsDir);
126
+ let totalBytes = 0;
127
+ let count = 0;
128
+ let skipped = 0;
129
+ for (const raw of body.dashboards) {
130
+ const entry = this.parseDashboardEntry(raw);
131
+ if (!entry) {
132
+ skipped++;
133
+ continue;
134
+ }
135
+ try {
136
+ totalBytes += await this.fetchDashboardFile(entry, dashboardsDir);
137
+ count++;
138
+ }
139
+ catch (err) {
140
+ skipped++;
141
+ this.log(`dashboard ${entry.name} failed: ${errMsg(err)}`);
142
+ }
143
+ }
144
+ return { count, skipped, totalBytes };
145
+ }
146
+ parseDashboardEntry(raw) {
147
+ if (!raw || typeof raw !== 'object')
148
+ return null;
149
+ const o = raw;
150
+ if (typeof o.name !== 'string' || !SAFE_FILENAME.test(o.name))
151
+ return null;
152
+ // SAFE_FILENAME matches "." and "..", which would resolve to the dir
153
+ // itself or its parent under join(). Server rejects these too, but
154
+ // belt-and-braces is cheap.
155
+ if (o.name === '.' || o.name === '..')
156
+ return null;
157
+ if (typeof o.sha256 !== 'string' || typeof o.bytes !== 'number')
158
+ return null;
159
+ return { name: o.name, sha256: o.sha256, bytes: o.bytes };
160
+ }
161
+ async fetchDashboardFile(entry, dashboardsDir) {
162
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/dashboards/${encodeURIComponent(entry.name)}`;
163
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
164
+ if (!res.ok || !res.body) {
165
+ throw new Error(`HTTP ${res.status}`);
166
+ }
167
+ return atomicWrite(res.body, join(dashboardsDir, entry.name));
168
+ }
169
+ async exportProvisioning(stagingDir) {
170
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/provisioning`;
171
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(10_000) });
172
+ if (!res.ok) {
173
+ throw new Error(`provisioning manifest HTTP ${res.status}`);
174
+ }
175
+ const body = (await res.json());
176
+ if (!Array.isArray(body.files)) {
177
+ throw new Error(`provisioning manifest missing 'files' array`);
178
+ }
179
+ const provisioningDir = join(stagingDir, 'provisioning');
180
+ // Reset so removed YAMLs don't linger in kopia forever.
181
+ await this.resetStagingDir(provisioningDir);
182
+ let totalBytes = 0;
183
+ let count = 0;
184
+ let skipped = 0;
185
+ for (const raw of body.files) {
186
+ const entry = this.parseProvisioningEntry(raw);
187
+ if (!entry) {
188
+ skipped++;
189
+ continue;
190
+ }
191
+ try {
192
+ totalBytes += await this.fetchProvisioningFile(entry, provisioningDir);
193
+ count++;
194
+ }
195
+ catch (err) {
196
+ skipped++;
197
+ this.log(`provisioning ${entry.relPath} failed: ${errMsg(err)}`);
198
+ }
199
+ }
200
+ return { count, skipped, totalBytes };
201
+ }
202
+ parseProvisioningEntry(raw) {
203
+ if (!raw || typeof raw !== 'object')
204
+ return null;
205
+ const o = raw;
206
+ if (typeof o.name !== 'string' || !SAFE_FILENAME.test(o.name))
207
+ return null;
208
+ if (typeof o.relPath !== 'string')
209
+ return null;
210
+ // Reject relPaths that try to escape via `..` or absolute paths
211
+ // before we use them in a host filesystem write. The server-side
212
+ // endpoint also rejects these, but defending in depth is cheap.
213
+ const segments = o.relPath.split('/').filter(Boolean);
214
+ if (segments.length === 0)
215
+ return null;
216
+ for (const seg of segments) {
217
+ if (seg === '..' || seg === '.' || !SAFE_FILENAME.test(seg))
218
+ return null;
219
+ }
220
+ if (typeof o.sha256 !== 'string' || typeof o.bytes !== 'number')
221
+ return null;
222
+ return { name: o.name, relPath: o.relPath, sha256: o.sha256, bytes: o.bytes };
223
+ }
224
+ async fetchProvisioningFile(entry, provisioningDir) {
225
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/provisioning/${encodeURIComponent(entry.relPath)}`;
226
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
227
+ if (!res.ok || !res.body) {
228
+ throw new Error(`HTTP ${res.status}`);
229
+ }
230
+ const finalPath = join(provisioningDir, entry.relPath);
231
+ await mkdir(dirname(finalPath), { recursive: true });
232
+ return atomicWrite(res.body, finalPath);
233
+ }
234
+ // Errors propagate so a failed reset reports as failed-section instead of emitting a half-fresh half-stale tree.
235
+ async resetStagingDir(root) {
236
+ await rm(root, { recursive: true, force: true });
237
+ await mkdir(root, { recursive: true });
238
+ }
239
+ }
240
+ // Failed writes unlink the partial so half-written data can't slip into the next snapshot.
241
+ async function atomicWrite(body, finalPath) {
242
+ const partialPath = `${finalPath}.partial`;
243
+ const nodeStream = Readable.fromWeb(body);
244
+ const sink = createWriteStream(partialPath);
245
+ let bytes = 0;
246
+ nodeStream.on('data', (chunk) => {
247
+ bytes += chunk.length;
248
+ });
249
+ try {
250
+ await pipeline(nodeStream, sink);
251
+ await rename(partialPath, finalPath);
252
+ return bytes;
253
+ }
254
+ catch (err) {
255
+ await unlink(partialPath).catch(() => undefined);
256
+ throw err;
257
+ }
258
+ }
259
+ function errMsg(err) {
260
+ return err instanceof Error ? err.message : String(err);
261
+ }
262
+ //# sourceMappingURL=grafana.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grafana.js","sourceRoot":"","sources":["../../src/database-export/grafana.ts"],"names":[],"mappings":"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"}
@@ -5,8 +5,8 @@
5
5
  * exporter pulls data via the source plugin's HTTP route on the SignalK
6
6
  * server itself — no container exec or shared filesystems involved.
7
7
  *
8
- * Currently supports QuestDB only. InfluxDB is intentionally out of
9
- * scope (see design doc).
8
+ * Supports QuestDB and Grafana. InfluxDB is intentionally out of scope
9
+ * (see design doc).
10
10
  */
11
11
  import type { ExportResult } from './types.js';
12
12
  export interface ExportOrchestratorOptions {
@@ -16,6 +16,11 @@ export interface ExportOrchestratorOptions {
16
16
  signalkBaseUrl: string;
17
17
  /** Optional debug logger. */
18
18
  log?: (msg: string) => void;
19
+ enabled?: {
20
+ questdb?: boolean;
21
+ grafana?: boolean;
22
+ signalkDatabase?: boolean;
23
+ };
19
24
  }
20
25
  /**
21
26
  * Run every supported exporter whose `detect()` returns true. Each
@@ -29,5 +34,7 @@ export interface ExportOrchestratorOptions {
29
34
  */
30
35
  export declare function runAllExports(opts: ExportOrchestratorOptions): Promise<ExportResult[]>;
31
36
  export type { DatabaseExporter, ExportResult, TableExport } from './types.js';
37
+ export { GrafanaExporter } from './grafana.js';
32
38
  export { QuestDBExporter } from './questdb.js';
39
+ export { SignalKDatabaseExporter } from './signalk-database.js';
33
40
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database-export/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,YAAY,CAAA;AAKhE,MAAM,WAAW,yBAAyB;IACxC,oDAAoD;IACpD,iBAAiB,EAAE,MAAM,CAAA;IACzB,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAA;IACtB,6BAA6B;IAC7B,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAgC5F;AAED,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database-export/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,YAAY,CAAA;AAKhE,MAAM,WAAW,yBAAyB;IACxC,oDAAoD;IACpD,iBAAiB,EAAE,MAAM,CAAA;IACzB,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAA;IACtB,6BAA6B;IAC7B,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAE3B,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAC9E;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAoD5F;AAED,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA"}
@@ -5,12 +5,14 @@
5
5
  * exporter pulls data via the source plugin's HTTP route on the SignalK
6
6
  * server itself — no container exec or shared filesystems involved.
7
7
  *
8
- * Currently supports QuestDB only. InfluxDB is intentionally out of
9
- * scope (see design doc).
8
+ * Supports QuestDB and Grafana. InfluxDB is intentionally out of scope
9
+ * (see design doc).
10
10
  */
11
11
  import { mkdir } from 'node:fs/promises';
12
12
  import { join } from 'node:path';
13
+ import { GrafanaExporter } from './grafana.js';
13
14
  import { QuestDBExporter } from './questdb.js';
15
+ import { SignalKDatabaseExporter } from './signalk-database.js';
14
16
  const PLUGIN_ID = 'signalk-backup';
15
17
  const STAGING_SUBDIR = 'database-exports';
16
18
  /**
@@ -26,12 +28,26 @@ const STAGING_SUBDIR = 'database-exports';
26
28
  export async function runAllExports(opts) {
27
29
  const stagingRoot = join(opts.signalkConfigRoot, 'plugin-config-data', PLUGIN_ID, STAGING_SUBDIR);
28
30
  await mkdir(stagingRoot, { recursive: true });
29
- const exporters = [
30
- new QuestDBExporter({
31
+ const enabled = opts.enabled ?? {};
32
+ const exporters = [];
33
+ if (enabled.questdb === true) {
34
+ exporters.push(new QuestDBExporter({
31
35
  signalkBaseUrl: opts.signalkBaseUrl,
32
36
  log: opts.log
33
- })
34
- ];
37
+ }));
38
+ }
39
+ if (enabled.grafana === true) {
40
+ exporters.push(new GrafanaExporter({
41
+ signalkBaseUrl: opts.signalkBaseUrl,
42
+ log: opts.log
43
+ }));
44
+ }
45
+ if (enabled.signalkDatabase === true) {
46
+ exporters.push(new SignalKDatabaseExporter({
47
+ signalkBaseUrl: opts.signalkBaseUrl,
48
+ log: opts.log
49
+ }));
50
+ }
35
51
  const results = [];
36
52
  for (const exporter of exporters) {
37
53
  if (!(await exporter.detect())) {
@@ -51,5 +67,7 @@ export async function runAllExports(opts) {
51
67
  }
52
68
  return results;
53
69
  }
70
+ export { GrafanaExporter } from './grafana.js';
54
71
  export { QuestDBExporter } from './questdb.js';
72
+ export { SignalKDatabaseExporter } from './signalk-database.js';
55
73
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/database-export/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAG9C,MAAM,SAAS,GAAG,gBAAgB,CAAA;AAClC,MAAM,cAAc,GAAG,kBAAkB,CAAA;AAWzC;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAA+B;IACjE,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,oBAAoB,EAAE,SAAS,EAAE,cAAc,CAAC,CAAA;IACjG,MAAM,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAE7C,MAAM,SAAS,GAAuB;QACpC,IAAI,eAAe,CAAC;YAClB,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,GAAG,EAAE,IAAI,CAAC,GAAG;SACd,CAAC;KACH,CAAA;IAED,MAAM,OAAO,GAAmB,EAAE,CAAA;IAClC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC,CAAC,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,GAAG,EAAE,CAAC,wBAAwB,QAAQ,CAAC,QAAQ,kBAAkB,CAAC,CAAA;YACvE,SAAQ;QACV,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAA;QACvD,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,UAAU,CAAC,CAAA;YAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACf,IAAI,CAAC,GAAG,EAAE,CACR,eAAe,QAAQ,CAAC,QAAQ,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,WAAW;gBAC7D,GAAG,CAAC,CAAC,UAAU,WAAW,CAAC,CAAC,UAAU,IAAI,CAC7C,CAAA;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,EAAE,CACR,eAAe,QAAQ,CAAC,QAAQ,YAAY,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC/F,CAAA;QACH,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAGD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/database-export/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { DatabaseExporter, ExportResult } from './types.js';
2
+ export interface SignalKDatabaseExporterOptions {
3
+ signalkBaseUrl?: string;
4
+ log?: (msg: string) => void;
5
+ /** Override fetch (tests). */
6
+ fetch?: typeof fetch;
7
+ }
8
+ export declare class SignalKDatabaseExporter implements DatabaseExporter {
9
+ readonly pluginId = "signalk-database";
10
+ private readonly baseUrl;
11
+ private readonly fetchImpl;
12
+ private readonly log;
13
+ constructor(opts?: SignalKDatabaseExporterOptions);
14
+ detect(): Promise<boolean>;
15
+ exportAll(stagingDir: string): Promise<ExportResult>;
16
+ private fetchManifest;
17
+ private parseEntry;
18
+ private exportOne;
19
+ private resetStagingDir;
20
+ }
21
+ //# sourceMappingURL=signalk-database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signalk-database.d.ts","sourceRoot":"","sources":["../../src/database-export/signalk-database.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAe,MAAM,YAAY,CAAA;AAS7E,MAAM,WAAW,8BAA8B;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3B,8BAA8B;IAC9B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACrB;AAQD,qBAAa,uBAAwB,YAAW,gBAAgB;IAC9D,QAAQ,CAAC,QAAQ,sBAA6B;IAE9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAuB;gBAE/B,IAAI,GAAE,8BAAmC;IAM/C,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAY1B,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;YAgD5C,aAAa;IAkB3B,OAAO,CAAC,UAAU;YAQJ,SAAS;YAmBT,eAAe;CAG9B"}