signalk-backup 0.4.0 → 0.6.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/README.md +35 -25
- package/package.json +6 -4
- package/plugin/config/image-tag.d.ts +3 -0
- package/plugin/config/image-tag.d.ts.map +1 -0
- package/plugin/config/image-tag.js +9 -0
- package/plugin/config/image-tag.js.map +1 -0
- package/plugin/config/schema.d.ts +2 -0
- package/plugin/config/schema.d.ts.map +1 -1
- package/plugin/config/schema.js +16 -4
- package/plugin/config/schema.js.map +1 -1
- package/plugin/database-export/grafana.d.ts +25 -0
- package/plugin/database-export/grafana.d.ts.map +1 -0
- package/plugin/database-export/grafana.js +262 -0
- package/plugin/database-export/grafana.js.map +1 -0
- package/plugin/database-export/index.d.ts +9 -2
- package/plugin/database-export/index.d.ts.map +1 -1
- package/plugin/database-export/index.js +24 -6
- package/plugin/database-export/index.js.map +1 -1
- package/plugin/database-export/signalk-database.d.ts +21 -0
- package/plugin/database-export/signalk-database.d.ts.map +1 -0
- package/plugin/database-export/signalk-database.js +148 -0
- package/plugin/database-export/signalk-database.js.map +1 -0
- package/plugin/database-export/staging-routes.d.ts +19 -0
- package/plugin/database-export/staging-routes.d.ts.map +1 -0
- package/plugin/database-export/staging-routes.js +205 -0
- package/plugin/database-export/staging-routes.js.map +1 -0
- package/plugin/index.d.ts.map +1 -1
- package/plugin/index.js +147 -44
- package/plugin/index.js.map +1 -1
- package/plugin/restore-host-write.d.ts +26 -0
- package/plugin/restore-host-write.d.ts.map +1 -0
- package/plugin/restore-host-write.js +363 -0
- package/plugin/restore-host-write.js.map +1 -0
- package/public/assets/{index-DMT6pdi_.js → index-C8j20cZ-.js} +2 -2
- package/public/assets/{index-DMT6pdi_.js.map → index-C8j20cZ-.js.map} +1 -1
- package/public/assets/main-B53J3gmZ.js +11 -0
- package/public/assets/main-B53J3gmZ.js.map +1 -0
- package/public/index.html +1 -1
- package/public/assets/main-BZ1xX0IR.js +0 -11
- package/public/assets/main-BZ1xX0IR.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
# signalk-backup
|
|
2
2
|
|
|
3
|
-
Scheduled deduplicated backups of your SignalK config (and optionally history) to local storage
|
|
3
|
+
Scheduled deduplicated backups of your SignalK config (and optionally history) to local storage, USB / NAS, or Google Drive — with a tree-browser that lets you restore individual files from any snapshot, anywhere your user can write.
|
|
4
4
|
|
|
5
|
-
The plugin
|
|
5
|
+
The plugin runs a React webapp inside the SignalK Admin UI and orchestrates a separate backup-engine container ([signalk-backup-server](https://github.com/dirkwa/signalk-backup-server)). All Kopia snapshot work happens in the container; the plugin handles the UI, the database-export tick, and host-side file writes for restores that target paths the container can't see.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Content-addressable deduplication** — Kopia stores file blobs once across all snapshots; identical config files between hourly/daily/weekly retention tiers cost nothing
|
|
10
|
-
- **Cloud sync to Google Drive** — optional, opt-in. Uses rclone with `drive.file` scope (the app only sees files it created)
|
|
11
10
|
- **Scheduled backups** — hourly / daily / weekly / on-startup tiers, each with independent retention
|
|
12
|
-
- **
|
|
11
|
+
- **Cloud + local destinations** — Google Drive (rclone, `drive.file` scope, opt-in OAuth), USB / NAS via local-fs, or SMB share. Snapshots stay in the local Kopia repo even when cloud sync is configured
|
|
12
|
+
- **Selective restore** — browse any backup's file tree, download a single file or whole subdirectory, or restore in-place. The trailing slash on a custom path means "land inside as a directory", so `tmp/` plus a file source writes `tmp/<filename>` instead of overwriting `tmp` as a file
|
|
13
|
+
- **Restore-anywhere** — custom-path restores stream through the plugin and write under the SignalK user's permissions, so you can land restored bytes in `/tmp`, `/media/usb/...`, or your home directory — anywhere your user can write
|
|
14
|
+
- **Restore with safety backup** — every full restore creates a snapshot of the current state first; partial restores stash the existing target as a sibling before overwriting. Both flows can be rolled back automatically on failure
|
|
15
|
+
- **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 backup to avoid torn-write hazards. The dedicated "Database exports" tab also extracts the export subtree from any historical backup without pulling the whole snapshot — a direct answer to "QuestDB shards bloat the full zip"
|
|
13
16
|
- **First-run safe default** — installs with a daily local-only schedule already seeded; no surprise data egress
|
|
14
17
|
- **Update detection** — checks ghcr.io for new container images via signalk-container's centralized update service
|
|
15
18
|
|
|
@@ -17,7 +20,7 @@ The plugin is a thin shell: it asks [signalk-container](https://github.com/dirkw
|
|
|
17
20
|
|
|
18
21
|
- SignalK Server ≥ 2.24.0
|
|
19
22
|
- Node 22 or newer (24 recommended)
|
|
20
|
-
- [signalk-container](https://github.com/dirkwa/signalk-container) ≥
|
|
23
|
+
- [signalk-container](https://github.com/dirkwa/signalk-container) ≥ 1.6.0 (provides the container runtime)
|
|
21
24
|
- Podman or Docker on the host
|
|
22
25
|
|
|
23
26
|
## Install
|
|
@@ -27,37 +30,44 @@ The plugin is a thin shell: it asks [signalk-container](https://github.com/dirkw
|
|
|
27
30
|
# (auto-enables on install — first-run schedules a daily local backup)
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
The plugin pulls `ghcr.io/dirkwa/signalk-backup-server
|
|
33
|
+
The plugin pulls `ghcr.io/dirkwa/signalk-backup-server` at a server version it pins itself (see [src/config/image-tag.ts](src/config/image-tag.ts)). Plugin releases and server releases are decoupled — you can pin or float either independently via the plugin's `imageTag` setting.
|
|
31
34
|
|
|
32
35
|
## How it works
|
|
33
36
|
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
└── /
|
|
37
|
-
└── plugin
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
```text
|
|
38
|
+
Browser
|
|
39
|
+
└── /signalk-backup/ ← plugin webapp (React)
|
|
40
|
+
└── /plugins/signalk-backup/api/* ← plugin's reverse proxy
|
|
41
|
+
├── /api/db-export/staging ← plugin-local (file lister)
|
|
42
|
+
├── /api/restore-partial-host ← plugin-local (writes to host fs)
|
|
43
|
+
└── /api/backups/*, /api/cloud/*, … ← proxied to backup-server container
|
|
44
|
+
└── http://127.0.0.1:<port>/api/*
|
|
45
|
+
(Kopia snapshots, restore-with-rollback,
|
|
46
|
+
partial-restore state machine, cloud sync)
|
|
40
47
|
```
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
The plugin is most of the user experience:
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
- The **webapp** at `/signalk-backup/` is a React SPA bundled by Vite. It runs in the SignalK admin origin and reads/writes via `fetch('/plugins/signalk-backup/api/*')`. No CORS dance, no separate auth — it inherits SignalK's.
|
|
52
|
+
- The **plugin process** owns three things directly: the database-export scheduler tick, an HTTP route that lists / streams files from the staging tree (so the webapp can show "live" exports), and an HTTP route that performs host-side file writes for custom-path restores.
|
|
53
|
+
- Everything else — Kopia snapshots, restore-with-rollback, cloud sync, scheduling — runs in the [signalk-backup-server](https://github.com/dirkwa/signalk-backup-server) container. The plugin reverse-proxies `/api/*` through to it, and adds a small set of explicit overrides above the catch-all proxy.
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
The webapp has four tabs:
|
|
47
56
|
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
- Cloud sync
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
- **Dashboard** — status, recent backups, scheduler next-run times
|
|
58
|
+
- **Backups** — create, list, download, restore (full or selective), upload a ZIP, delete
|
|
59
|
+
- **Database exports** — live staging table (one row per exporter file currently on disk) plus a "pick a backup → browse the database-exports subtree" extractor for grabbing historical shards without pulling the whole snapshot
|
|
60
|
+
- **Cloud sync** — provider chooser (Google Drive / local / SMB), credentials, sync mode, on-demand sync
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
54
63
|
|
|
55
|
-
The SignalK Admin UI
|
|
64
|
+
Most settings live in the **webapp** under Settings. The plugin's SignalK Admin UI config panel is intentionally small and handles only deployment-level options (the schema in [src/config/schema.ts](src/config/schema.ts) is the source of truth — update this list if you bump it):
|
|
56
65
|
|
|
57
|
-
- `managedContainer` —
|
|
58
|
-
- `imageTag` — pin a specific version or use
|
|
66
|
+
- `managedContainer` — let the plugin manage the backup-server container (default), or point at an external instance
|
|
67
|
+
- `imageTag` — pin a specific version or use `auto` (default — resolves to the BACKUP_SERVER_VERSION constant)
|
|
59
68
|
- `externalUrl` — only used when `managedContainer: false`
|
|
60
|
-
- `
|
|
69
|
+
- `databaseExport.{questdb, grafana, signalkDatabase}` — enable per-exporter (default off; users opt in once their DB plugin is producing data)
|
|
70
|
+
- `databaseExport.intervalMinutes` — how often the export tick runs (5–1440)
|
|
61
71
|
|
|
62
72
|
## License
|
|
63
73
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-backup",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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
|
|
10
|
-
"lint": "eslint
|
|
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",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@sinclair/typebox": "^0.34.0",
|
|
52
|
-
"bonjour-service": "^1.3.0"
|
|
52
|
+
"bonjour-service": "^1.3.0",
|
|
53
|
+
"unzipper": "^0.12.3"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"signalk-container": ">=1.6.0"
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"@types/react": "^19.2.14",
|
|
69
70
|
"@types/react-dom": "^19.2.3",
|
|
70
71
|
"@types/supertest": "^7.2.0",
|
|
72
|
+
"@types/unzipper": "^0.10.11",
|
|
71
73
|
"@vitejs/plugin-react": "^6.0.1",
|
|
72
74
|
"bootstrap": "^5.3.8",
|
|
73
75
|
"eslint": "^10.3.0",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-tag.d.ts","sourceRoot":"","sources":["../../src/config/image-tag.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,qBAAqB,UAAU,CAAA;AAE5C,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The signalk-backup-server image version that "auto" resolves to.
|
|
2
|
+
// Bump this when a new signalk-backup-server release is published to ghcr.io.
|
|
3
|
+
// Independent of signalk-backup's own package.json version — the two repos
|
|
4
|
+
// release on independent cadences. See AGENTS.md "Gotchas" for rationale.
|
|
5
|
+
export const BACKUP_SERVER_VERSION = '0.5.0';
|
|
6
|
+
export function resolveImageTag(tag) {
|
|
7
|
+
return tag === 'auto' ? BACKUP_SERVER_VERSION : tag;
|
|
8
|
+
}
|
|
9
|
+
//# 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,mEAAmE;AACnE,8EAA8E;AAC9E,2EAA2E;AAC3E,0EAA0E;AAC1E,MAAM,CAAC,MAAM,qBAAqB,GAAG,OAAO,CAAA;AAE5C,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,OAAO,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,GAAG,CAAA;AACrD,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
|
|
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"}
|
package/plugin/config/schema.js
CHANGED
|
@@ -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: '
|
|
20
|
+
default: 'auto',
|
|
21
21
|
title: 'Container image tag',
|
|
22
|
-
description: '
|
|
22
|
+
description: '"auto" (default) tracks the signalk-backup-server version this plugin release was tested against. ' +
|
|
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
|
-
}, {
|
|
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: '
|
|
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,
|
|
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,oGAAoG;YACpG,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
|
-
*
|
|
9
|
-
*
|
|
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;
|
|
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"}
|