@signalk/charts-plugin 3.7.0 → 3.8.1

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 (60) hide show
  1. package/.prettierignore +1 -0
  2. package/CHANGELOG.md +109 -0
  3. package/README.md +72 -0
  4. package/docs/screenshots/1_overview.png +0 -0
  5. package/docs/screenshots/2_region.png +0 -0
  6. package/docs/screenshots/3_jobs.png +0 -0
  7. package/package.json +16 -11
  8. package/plugin/cacheLayout.d.ts +72 -0
  9. package/plugin/cacheLayout.d.ts.map +1 -0
  10. package/plugin/cacheLayout.js +243 -0
  11. package/plugin/cacheLayout.js.map +1 -0
  12. package/plugin/chartDownloader.d.ts +122 -29
  13. package/plugin/chartDownloader.d.ts.map +1 -1
  14. package/plugin/chartDownloader.js +491 -314
  15. package/plugin/chartDownloader.js.map +1 -1
  16. package/plugin/chartDownloaderMBTilesHelpers.d.ts +10 -0
  17. package/plugin/chartDownloaderMBTilesHelpers.d.ts.map +1 -0
  18. package/plugin/chartDownloaderMBTilesHelpers.js +202 -0
  19. package/plugin/chartDownloaderMBTilesHelpers.js.map +1 -0
  20. package/plugin/chartDownloaderTileHelpers.d.ts +30 -0
  21. package/plugin/chartDownloaderTileHelpers.d.ts.map +1 -0
  22. package/plugin/chartDownloaderTileHelpers.js +277 -0
  23. package/plugin/chartDownloaderTileHelpers.js.map +1 -0
  24. package/plugin/charts.d.ts.map +1 -1
  25. package/plugin/charts.js +24 -39
  26. package/plugin/charts.js.map +1 -1
  27. package/plugin/constants.d.ts.map +1 -1
  28. package/plugin/index.d.ts.map +1 -1
  29. package/plugin/index.js +564 -47
  30. package/plugin/index.js.map +1 -1
  31. package/plugin/pluginStatus.js.map +1 -1
  32. package/plugin/projection.js.map +1 -1
  33. package/plugin/tileServer.d.ts +4 -2
  34. package/plugin/tileServer.d.ts.map +1 -1
  35. package/plugin/tileServer.js +37 -8
  36. package/plugin/tileServer.js.map +1 -1
  37. package/plugin/tokenProvider.d.ts +63 -0
  38. package/plugin/tokenProvider.d.ts.map +1 -0
  39. package/plugin/tokenProvider.js +258 -0
  40. package/plugin/tokenProvider.js.map +1 -0
  41. package/plugin/types.d.ts +37 -2
  42. package/plugin/types.d.ts.map +1 -1
  43. package/public/images/jobs.png +0 -0
  44. package/public/images/overview.png +0 -0
  45. package/public/images/region.png +0 -0
  46. package/public/index.html +392 -113
  47. package/public/js/index.js +620 -0
  48. package/public/leaflet/images/downloads.png +0 -0
  49. package/public/leaflet/images/layers-2x.png +0 -0
  50. package/public/leaflet/images/layers.png +0 -0
  51. package/public/leaflet/images/lifebuoy.png +0 -0
  52. package/public/leaflet/images/spritesheet-2x.png +0 -0
  53. package/public/leaflet/images/spritesheet.svg +156 -0
  54. package/public/leaflet/leaflet.css +677 -0
  55. package/public/leaflet/leaflet.draw.css +253 -0
  56. package/public/leaflet/leaflet.draw.js +2957 -0
  57. package/public/leaflet/leaflet.js +7467 -0
  58. package/public/style.css +351 -37
  59. package/docs/screenshots/1_placeholder.png +0 -0
  60. package/public/index.js +0 -349
package/.prettierignore CHANGED
@@ -5,3 +5,4 @@ coverage
5
5
  plugin
6
6
  reports
7
7
  .stryker-tmp
8
+ public/leaflet
package/CHANGELOG.md ADDED
@@ -0,0 +1,109 @@
1
+ # Changelog
2
+
3
+ This file tracks user-visible changes between released versions. For the
4
+ full commit history, see the [git log](https://github.com/SignalK/charts-plugin/commits/master)
5
+ or the [GitHub releases](https://github.com/SignalK/charts-plugin/releases).
6
+
7
+ ## v3.7.0 (unreleased)
8
+
9
+ The big change in 3.7 is the `ChartDownloader` rewrite (PR #84) plus the
10
+ follow-on architectural cleanup. Highlights:
11
+
12
+ ### Added
13
+
14
+ - **Tile cache moved to mbtiles SQLite.** Per-provider working files at
15
+ `<cachePath>/.working/<id>/cache.mbtiles` replace the per-tile PNG layout
16
+ used in 3.6 and earlier. Faster on slow storage (Pi SD cards), survives
17
+ filesystem hiccups better, no inode pressure.
18
+ - **Region snapshot exports** with `options.mbtiles: true` on a seeding
19
+ job. Output lands in `<cachePath>/exports/<region>_<provider-id>.mbtiles`
20
+ and is excluded from the chart scanner so it can be safely copied to
21
+ another Signal K server (e.g. via USB).
22
+ - **Leaflet-based webapp** with live region drawing, multi-region seeding,
23
+ refresh-all-tiles and vacuum options, persistent regions storage, and
24
+ mobile-friendly layout. Replaces the earlier form-based UI.
25
+ - **Declarative token providers** (`tokenProviders` config) for chart
26
+ sources that need a rotating bearer token fetched from a separate URL
27
+ (Navionics-via-Garmin, ArcGIS key rotation, OAuth client_credentials).
28
+ Token fetched on demand, cached for `ttlSeconds`, templated into the
29
+ tile URL and headers via `{token.<field>}` placeholders. Replaces the
30
+ early-iteration `.js` chart-provider auto-loader (dropped for security).
31
+ See README for the schema.
32
+ - **Legacy PNG → mbtiles migration tool** at
33
+ `POST /signalk/chart-tiles/cache/<id>/migrate`. Walks an existing
34
+ pre-3.7 per-file cache and inserts each tile into the new mbtiles.
35
+ Idempotent, fire-and-forget, with status polling at the same URL via
36
+ GET. Solves the upgrade-and-lose-cache regression.
37
+ - **New endpoints**: `GET /cache/stats` (per-provider hit/miss/failure
38
+ counters), `GET/POST /cache/regions` (persistent region storage for
39
+ the webapp), `GET /cache/<id>/migrate` (migration status).
40
+ - **CHANGELOG.md** (this file).
41
+
42
+ ### Changed
43
+
44
+ - Chart-folder scanner excludes `.working/`, `exports/`, and any
45
+ dot-prefixed entry at depth 0. User charts at any other path are
46
+ scanned as before.
47
+ - Scanned mbtiles files are opened read-only (already in 3.6 via
48
+ `e7eb431`); 3.7 pairs that with the working/exports directory split
49
+ so the proxy and the scanner never compete for write access on the
50
+ same file.
51
+ - Tile lists during seeding are generators rather than
52
+ pre-materialised arrays. Bounds memory on large bboxes; fixes the
53
+ OOM seen at high zoom levels (#48).
54
+ - Disk-space safety check moved into the worker hot path (every 1000
55
+ tiles). When space frees up, caching resumes — the previous
56
+ one-way `CachingDisabled` latch is gone.
57
+ - `POST /signalk/chart-tiles/cache/jobs/<id>` with `action='start'`
58
+ rejects non-Idle jobs with `409 Conflict`. The webapp already hid
59
+ the button; the endpoint is now consistent with that. Remove and
60
+ recreate the job to seed again.
61
+ - Token fetches are coalesced: concurrent tile requests share one
62
+ in-flight token fetch instead of N parallel ones. An upstream
63
+ 401/403 invalidates the cached token so the next request refetches.
64
+
65
+ ### Fixed / Security
66
+
67
+ - The legacy-cache migration endpoint now refuses a `sourceName` that
68
+ resolves onto an installed directory chart (one carrying
69
+ `tilemapresource.xml` / `metadata.json`). Because `cachePath` defaults
70
+ to the chart-scan path and directory charts share the
71
+ `<z>/<x>/<y>` tile layout the migrator walks, `deleteSource: true`
72
+ could otherwise have deleted a user's installed chart.
73
+ - Seed and delete jobs are fire-and-forget; a failure inside one
74
+ (locked DB, disk error) is now caught and recorded on the job instead
75
+ of surfacing as an unhandled promise rejection, which on Node 22+
76
+ would terminate the Signal K server.
77
+ - `remoteUrl` and request `headers` are no longer included in the
78
+ `/resources/charts` view. For token providers these resolve a live
79
+ bearer token; clients fetch tiles through the proxy path and never
80
+ need the upstream target, so exposing them leaked the credential.
81
+ - `@signalk/mbtiles` (and its `node:sqlite` dependency) is loaded
82
+ lazily, so a server on Node < 22 shows the plugin's "requires Node
83
+ 22" message instead of crashing on `ERR_UNKNOWN_BUILTIN_MODULE`
84
+ before the guard can run. `engines.node` is set accordingly.
85
+ - The writable mbtiles handle attaches an `error` listener so a runtime
86
+ SQLite error (e.g. disk full on an SD card) is logged rather than
87
+ crashing the process.
88
+
89
+ ### Migration / upgrade notes
90
+
91
+ - On first start after upgrade, the plugin moves any pre-3.7
92
+ `<cachePath>/<id>.mbtiles_` and `<cachePath>/mbtiles/` files into
93
+ the new `.working/` and `exports/` layout. The migration is
94
+ idempotent, no admin action required.
95
+ - Existing per-file PNG caches are NOT automatically imported. To
96
+ preserve them, run `POST /signalk/chart-tiles/cache/<id>/migrate`
97
+ per provider after upgrading. See README.
98
+ - The `.js` chart-provider auto-loader from early PR #84 iterations
99
+ was removed. Replace any such modules with a `tokenProviders`
100
+ config entry.
101
+
102
+ ### Internal
103
+
104
+ - `ChartDownloader` split across `src/chartDownloader.ts`,
105
+ `src/chartDownloaderTileHelpers.ts`, and
106
+ `src/chartDownloaderMBTilesHelpers.ts` for testability.
107
+ - ~200 unit + HTTP integration tests added covering tile-coordinate
108
+ math, the SQLite helpers, token-provider TTL/template/401 paths,
109
+ cache-layout migration, and the new HTTP endpoints.
package/README.md CHANGED
@@ -99,6 +99,78 @@ Example: NOAA Chart Display Service (S-57 ENC) as a cacheable tile source — se
99
99
  https://gis.charttools.noaa.gov/arcgis/rest/services/MCS/NOAAChartDisplay/MapServer/exts/MaritimeChartService/WMSServer?service=WMS&version=1.3.0&request=GetMap&layers=0,1,2,3,4,5,6,7,8,9,10,11,12&styles=&crs=EPSG:3857&bbox={bbox_3857}&width=256&height=256&format=image/png&transparent=true
100
100
  ```
101
101
 
102
+ ### Token providers (declarative dynamic providers)
103
+
104
+ Some commercial chart sources require a short-lived bearer token fetched from a separate endpoint and rotated periodically (Navionics-via-Garmin, ArcGIS-style key rotation, OAuth client_credentials, etc). Configure these via `tokenProviders` in the plugin config. The plugin fetches the token, caches it for `ttlSeconds`, and templates it into the tile URL and headers per request. No code execution — declarative JSON only.
105
+
106
+ ```json
107
+ {
108
+ "tokenProviders": [
109
+ {
110
+ "identifier": "navionics",
111
+ "name": "Navionics",
112
+ "type": "tilelayer",
113
+ "format": "png",
114
+ "scale": 50000,
115
+ "minzoom": 2,
116
+ "maxzoom": 19,
117
+ "bounds": [-180, -90, 180, 90],
118
+ "tokenEndpoint": {
119
+ "url": "https://example.com/api/token",
120
+ "method": "GET",
121
+ "headers": { "User-Agent": "..." },
122
+ "ttlSeconds": 600
123
+ },
124
+ "tile": {
125
+ "url": "https://tile{1-5}.example.com/v1/tile/{z}/{x}/{y}?config={token.configuration_token}",
126
+ "headers": { "Authorization": "Bearer {token.access_token}" }
127
+ }
128
+ }
129
+ ]
130
+ }
131
+ ```
132
+
133
+ Template placeholders inside `tile.url` and `tile.headers`:
134
+
135
+ | Placeholder | Replaced with |
136
+ | ----------------- | ------------------------------------------------------------------ |
137
+ | `{token.<field>}` | A string-valued field from the latest token-endpoint JSON response |
138
+ | `{a-b}` | A random integer in `[a, b]` inclusive (sharded tile hostnames) |
139
+ | `{z}` `{x}` `{y}` | Tile coords (substituted by the standard tile fetch path) |
140
+
141
+ Token fetches are coalesced — concurrent tile requests share one in-flight token fetch — and an upstream 401/403 invalidates the cached token so the next request refetches it. A token-endpoint failure is logged but doesn't take down tile requests; subsequent fetches retry.
142
+
143
+ > [!WARNING]
144
+ > The plugin executes only the configured HTTP requests. The configs themselves describe how to talk to a third-party service: confirm the provider's terms of use allow it before deploying. SignalK itself ships no provider configs that may break a third party's TOS.
145
+
146
+ ### Cache directory layout
147
+
148
+ When `cachePath` is set (or defaulted to `~/.signalk/charts/`) the plugin organises its files into three subdirectories that the chart scanner treats differently:
149
+
150
+ | Subdir | Scanned? | Purpose |
151
+ | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
152
+ | `.working/` | No | Per-provider working mbtiles caches (`<id>/cache.mbtiles`); written to by the proxy. |
153
+ | `exports/` | No | Region snapshot mbtiles produced by seeding jobs with the "separate file" option. Intended for offline transfer (USB stick to another SK server). |
154
+ | (any other) | Yes | Charts the user installed; opened read-only by the scanner. |
155
+
156
+ The scanner also skips any top-level entry whose name starts with `.`. On startup the plugin migrates pre-3.7 layouts (`<id>.mbtiles_` and `mbtiles/` subdir) into this shape automatically; the migration is idempotent so a repeat startup is a no-op.
157
+
158
+ ### Migrating a pre-3.7 PNG cache
159
+
160
+ Versions before 3.7 stored proxy-cached tiles as one PNG per tile under `<cachePath>/<provider.name>/<z>/<x>/<y>.<format>`. The 3.7 release moves all caching into mbtiles. To reuse an existing PNG cache instead of re-downloading:
161
+
162
+ ```bash
163
+ # Kick off the migration (returns 202 immediately)
164
+ curl -X POST http://localhost:3000/signalk/chart-tiles/cache/<provider-id>/migrate \
165
+ -H 'Content-Type: application/json' \
166
+ -d '{"sourceName": "<legacy-dir-name>", "deleteSource": false}'
167
+
168
+ # Poll progress / final counts
169
+ curl http://localhost:3000/signalk/chart-tiles/cache/<provider-id>/migrate
170
+ ```
171
+
172
+ `sourceName` defaults to `provider.name` (the human-readable name the old cache was keyed under). `deleteSource: true` removes each PNG after a successful insert; the default keeps the source files in place so a botched migration is recoverable. The migration is idempotent — re-running after a failure picks up where the last attempt left off.
173
+
102
174
  ### Supported chart formats
103
175
 
104
176
  - [MBTiles](https://github.com/mapbox/mbtiles-spec) files
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalk/charts-plugin",
3
- "version": "3.7.0",
3
+ "version": "3.8.1",
4
4
  "description": "Signal K plugin to provide chart support for Signal K server",
5
5
  "main": "plugin/index.js",
6
6
  "scripts": {
@@ -19,6 +19,9 @@
19
19
  "prepublishOnly": "npm run build"
20
20
  },
21
21
  "license": "Apache-2.0",
22
+ "engines": {
23
+ "node": ">=24"
24
+ },
22
25
  "keywords": [
23
26
  "signalk",
24
27
  "signalk-node-server-plugin",
@@ -36,7 +39,9 @@
36
39
  "displayName": "SK Charts",
37
40
  "appIcon": "./logo.svg",
38
41
  "screenshots": [
39
- "./docs/screenshots/1_placeholder.png"
42
+ "./docs/screenshots/1_overview.png",
43
+ "./docs/screenshots/2_region.png",
44
+ "./docs/screenshots/3_jobs.png"
40
45
  ]
41
46
  },
42
47
  "dependencies": {
@@ -45,7 +50,7 @@
45
50
  "@turf/bbox": "^7.2.0",
46
51
  "@turf/boolean-intersects": "^7.2.0",
47
52
  "@turf/helpers": "^7.2.0",
48
- "check-disk-space": "^3.4.0",
53
+ "check-disk-space": "^4.0.0",
49
54
  "fast-xml-parser": "^5.7.1",
50
55
  "geojson-antimeridian-cut": "^0.1.0",
51
56
  "lodash": "^4.17.11",
@@ -53,24 +58,24 @@
53
58
  },
54
59
  "repository": {
55
60
  "type": "git",
56
- "url": "https://github.com/SignalK/charts-plugin"
61
+ "url": "git+https://github.com/SignalK/charts-plugin.git"
57
62
  },
58
63
  "devDependencies": {
59
- "@stryker-mutator/core": "^9.6.1",
60
- "@stryker-mutator/mocha-runner": "^9.6.1",
64
+ "@stryker-mutator/core": "^10.0.0",
65
+ "@stryker-mutator/mocha-runner": "^10.0.0",
61
66
  "@types/chai": "^5.2.3",
62
67
  "@types/express": "^5.0.6",
63
68
  "@types/lodash": "^4.14.191",
64
69
  "@types/mocha": "^10.0.10",
65
- "@types/node": "^25.6.0",
66
- "c8": "^11.0.0",
67
- "chai": "6.2.2",
70
+ "@types/node": "^26.1.0",
71
+ "c8": "^12.0.0",
72
+ "chai": "6.3.0",
68
73
  "chai-http": "^5.1.2",
69
74
  "express": "^5.2.1",
70
- "mocha": "^11.7.1",
75
+ "mocha": "^12.0.0",
71
76
  "prettier": "^3.8.2",
72
77
  "rimraf": "^6.1.3",
73
78
  "tsx": "^4.20.3",
74
- "typescript": "^6.0.3"
79
+ "typescript": "^7.0.2"
75
80
  }
76
81
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Cache directory layout.
3
+ *
4
+ * Three roles inside `cachePath`, kept as separate subdirectories so the
5
+ * scanner can tell them apart from user-installed charts:
6
+ *
7
+ * ${cachePath}/
8
+ * .working/<provider-id>/cache.mbtiles - proxy working cache (RW), not scanned
9
+ * exports/<name>_<provider-id>.mbtiles - region snapshots for offline transfer, not scanned
10
+ *
11
+ * The scanner (src/charts.ts) excludes `.working` and `exports` by name at
12
+ * depth 0, plus any dot-prefixed entry. Anything else under `cachePath` is
13
+ * treated as a user chart and read-opened.
14
+ *
15
+ * `migrateLegacyCacheLayout()` is run once per startup to move any
16
+ * flat-layout proxy files (`<id>.mbtiles_`) and the older `mbtiles/`
17
+ * subdirectory of region snapshots into the layout above.
18
+ */
19
+ import type { MBTilesHandle } from './types';
20
+ export declare const WORKING_DIR = ".working";
21
+ export declare const EXPORTS_DIR = "exports";
22
+ /** Absolute path to a proxy provider's working mbtiles cache. */
23
+ export declare function workingMbtilesPath(cachePath: string, providerIdentifier: string): string;
24
+ /** Absolute path to a region-snapshot export file. */
25
+ export declare function exportMbtilesPath(cachePath: string, regionName: string, providerIdentifier: string): string;
26
+ /**
27
+ * Move any flat-layout proxy / export files into the .working/ and
28
+ * exports/ subdirs the scanner expects. Idempotent: re-running after
29
+ * success is a no-op. Failures on individual files are logged and
30
+ * skipped so a partial migration doesn't take the plugin out — the next
31
+ * startup retries.
32
+ *
33
+ * Migrates:
34
+ * ${cachePath}/${id}.mbtiles_ -> .working/${id}/cache.mbtiles
35
+ * ${cachePath}/mbtiles/${name}_${id}.mbtiles -> exports/${name}_${id}.mbtiles
36
+ */
37
+ export declare function migrateLegacyCacheLayout(cachePath: string, app: {
38
+ debug: (msg: string) => void;
39
+ }): Promise<void>;
40
+ /**
41
+ * Counts returned by migrateLegacyTileCache. `migrated` is tiles inserted
42
+ * into mbtiles; `skipped` is non-matching files (subdirs / extensions we
43
+ * don't recognise / a stray README); `failed` is files we tried to migrate
44
+ * and couldn't (read or putTile errored). Distinguishing skip from fail
45
+ * matters when the admin reads the response: a high `failed` count is a
46
+ * real problem, a high `skipped` count usually isn't.
47
+ */
48
+ export interface LegacyMigrationCounts {
49
+ migrated: number;
50
+ skipped: number;
51
+ failed: number;
52
+ }
53
+ /**
54
+ * Walk a flat-file tile cache rooted at `legacyDir`,
55
+ * `<root>/<z>/<x>/<y>.<ext>`, and insert each tile into the provider's
56
+ * working mbtiles via the standard putTile callback API.
57
+ *
58
+ * Idempotent: re-running after partial failure picks up where the last
59
+ * run left off (mbtiles' INSERT OR IGNORE-style write makes duplicate
60
+ * inserts cheap). `deleteSource: true` removes successfully migrated
61
+ * files so the source directory drains over time; the directory itself
62
+ * is left in place even when empty (admin tooling can rmdir later).
63
+ *
64
+ * Failures on individual tiles are counted and the walk continues:
65
+ * one corrupt PNG shouldn't block 49,999 healthy ones.
66
+ */
67
+ export declare function migrateLegacyTileCache(legacyDir: string, mbtilesHandle: MBTilesHandle, opts?: {
68
+ deleteSource?: boolean;
69
+ onProgress?: (counts: LegacyMigrationCounts) => void;
70
+ signal?: AbortSignal;
71
+ }): Promise<LegacyMigrationCounts>;
72
+ //# sourceMappingURL=cacheLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cacheLayout.d.ts","sourceRoot":"","sources":["../src/cacheLayout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,eAAO,MAAM,WAAW,aAAa,CAAA;AACrC,eAAO,MAAM,WAAW,YAAY,CAAA;AAEpC,iEAAiE;AACjE,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,MAAM,GACzB,MAAM,CAER;AAED,sDAAsD;AACtD,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,kBAAkB,EAAE,MAAM,GACzB,MAAM,CAMR;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE;IAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,OAAO,CAAC,IAAI,CAAC,CAgEf;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,aAAa,EAC5B,IAAI,GAAE;IACJ,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAA;IAKpD,MAAM,CAAC,EAAE,WAAW,CAAA;CAChB,GACL,OAAO,CAAC,qBAAqB,CAAC,CA+FhC"}
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ /**
3
+ * Cache directory layout.
4
+ *
5
+ * Three roles inside `cachePath`, kept as separate subdirectories so the
6
+ * scanner can tell them apart from user-installed charts:
7
+ *
8
+ * ${cachePath}/
9
+ * .working/<provider-id>/cache.mbtiles - proxy working cache (RW), not scanned
10
+ * exports/<name>_<provider-id>.mbtiles - region snapshots for offline transfer, not scanned
11
+ *
12
+ * The scanner (src/charts.ts) excludes `.working` and `exports` by name at
13
+ * depth 0, plus any dot-prefixed entry. Anything else under `cachePath` is
14
+ * treated as a user chart and read-opened.
15
+ *
16
+ * `migrateLegacyCacheLayout()` is run once per startup to move any
17
+ * flat-layout proxy files (`<id>.mbtiles_`) and the older `mbtiles/`
18
+ * subdirectory of region snapshots into the layout above.
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.EXPORTS_DIR = exports.WORKING_DIR = void 0;
25
+ exports.workingMbtilesPath = workingMbtilesPath;
26
+ exports.exportMbtilesPath = exportMbtilesPath;
27
+ exports.migrateLegacyCacheLayout = migrateLegacyCacheLayout;
28
+ exports.migrateLegacyTileCache = migrateLegacyTileCache;
29
+ const path_1 = __importDefault(require("path"));
30
+ const fs_1 = require("fs");
31
+ exports.WORKING_DIR = '.working';
32
+ exports.EXPORTS_DIR = 'exports';
33
+ /** Absolute path to a proxy provider's working mbtiles cache. */
34
+ function workingMbtilesPath(cachePath, providerIdentifier) {
35
+ return path_1.default.join(cachePath, exports.WORKING_DIR, providerIdentifier, 'cache.mbtiles');
36
+ }
37
+ /** Absolute path to a region-snapshot export file. */
38
+ function exportMbtilesPath(cachePath, regionName, providerIdentifier) {
39
+ return path_1.default.join(cachePath, exports.EXPORTS_DIR, `${regionName}_${providerIdentifier}.mbtiles`);
40
+ }
41
+ /**
42
+ * Move any flat-layout proxy / export files into the .working/ and
43
+ * exports/ subdirs the scanner expects. Idempotent: re-running after
44
+ * success is a no-op. Failures on individual files are logged and
45
+ * skipped so a partial migration doesn't take the plugin out — the next
46
+ * startup retries.
47
+ *
48
+ * Migrates:
49
+ * ${cachePath}/${id}.mbtiles_ -> .working/${id}/cache.mbtiles
50
+ * ${cachePath}/mbtiles/${name}_${id}.mbtiles -> exports/${name}_${id}.mbtiles
51
+ */
52
+ async function migrateLegacyCacheLayout(cachePath, app) {
53
+ // Flat-layout proxy files (.mbtiles_ trailing underscore).
54
+ let entries;
55
+ try {
56
+ entries = await fs_1.promises.readdir(cachePath, { withFileTypes: true });
57
+ }
58
+ catch {
59
+ // Cache path doesn't exist yet — nothing to migrate.
60
+ return;
61
+ }
62
+ for (const entry of entries) {
63
+ if (!entry.isFile())
64
+ continue;
65
+ const m = entry.name.match(/^(.+)\.mbtiles_$/);
66
+ if (!m)
67
+ continue;
68
+ const id = m[1];
69
+ const src = path_1.default.join(cachePath, entry.name);
70
+ const dst = workingMbtilesPath(cachePath, id);
71
+ try {
72
+ await fs_1.promises.mkdir(path_1.default.dirname(dst), { recursive: true });
73
+ await fs_1.promises.rename(src, dst);
74
+ app.debug(`Migrated proxy cache ${src} -> ${dst}`);
75
+ }
76
+ catch (err) {
77
+ app.debug(`Could not migrate ${src} -> ${dst}: ${err.message}`);
78
+ }
79
+ }
80
+ // Flat-layout region-snapshot directory (cachePath/mbtiles/).
81
+ const oldExportDir = path_1.default.join(cachePath, 'mbtiles');
82
+ let oldExports = [];
83
+ try {
84
+ oldExports = await fs_1.promises.readdir(oldExportDir, { withFileTypes: true });
85
+ }
86
+ catch (err) {
87
+ // ENOENT: legacy dir doesn't exist, nothing to migrate. Any other
88
+ // error (EACCES, EBUSY, EIO) means the dir might exist but we
89
+ // couldn't read it — surface that so the admin sees half-migrated
90
+ // state instead of a silent skip.
91
+ const code = err.code;
92
+ if (code !== 'ENOENT') {
93
+ console.warn(`[charts-plugin] could not read legacy export dir ${oldExportDir}: ` +
94
+ `${err.message} ` +
95
+ `(proxy-cache migration above may have completed; manual cleanup needed)`);
96
+ }
97
+ return;
98
+ }
99
+ for (const entry of oldExports) {
100
+ if (!entry.isFile() || !entry.name.match(/\.mbtiles$/i))
101
+ continue;
102
+ const src = path_1.default.join(oldExportDir, entry.name);
103
+ const dst = path_1.default.join(cachePath, exports.EXPORTS_DIR, entry.name);
104
+ try {
105
+ await fs_1.promises.mkdir(path_1.default.dirname(dst), { recursive: true });
106
+ await fs_1.promises.rename(src, dst);
107
+ app.debug(`Migrated export ${src} -> ${dst}`);
108
+ }
109
+ catch (err) {
110
+ app.debug(`Could not migrate ${src} -> ${dst}: ${err.message}`);
111
+ }
112
+ }
113
+ // Best-effort cleanup of the now-empty legacy export dir; ignore failure
114
+ // (e.g. user dropped a non-mbtiles file in there manually).
115
+ try {
116
+ await fs_1.promises.rmdir(oldExportDir);
117
+ }
118
+ catch {
119
+ // leave it; harmless
120
+ }
121
+ }
122
+ const TILE_EXTENSION = /\.(png|jpe?g|webp|pbf|mvt)$/i;
123
+ /**
124
+ * Walk a flat-file tile cache rooted at `legacyDir`,
125
+ * `<root>/<z>/<x>/<y>.<ext>`, and insert each tile into the provider's
126
+ * working mbtiles via the standard putTile callback API.
127
+ *
128
+ * Idempotent: re-running after partial failure picks up where the last
129
+ * run left off (mbtiles' INSERT OR IGNORE-style write makes duplicate
130
+ * inserts cheap). `deleteSource: true` removes successfully migrated
131
+ * files so the source directory drains over time; the directory itself
132
+ * is left in place even when empty (admin tooling can rmdir later).
133
+ *
134
+ * Failures on individual tiles are counted and the walk continues:
135
+ * one corrupt PNG shouldn't block 49,999 healthy ones.
136
+ */
137
+ async function migrateLegacyTileCache(legacyDir, mbtilesHandle, opts = {}) {
138
+ const counts = {
139
+ migrated: 0,
140
+ skipped: 0,
141
+ failed: 0
142
+ };
143
+ let zEntries;
144
+ try {
145
+ zEntries = await fs_1.promises.readdir(legacyDir, { withFileTypes: true });
146
+ }
147
+ catch (err) {
148
+ // Missing legacyDir is the same as "nothing to migrate" — return zeros.
149
+ if (err.code === 'ENOENT')
150
+ return counts;
151
+ throw err;
152
+ }
153
+ for (const zEntry of zEntries) {
154
+ if (opts.signal?.aborted)
155
+ return counts;
156
+ if (!zEntry.isDirectory()) {
157
+ counts.skipped++;
158
+ continue;
159
+ }
160
+ const z = parseInt(zEntry.name, 10);
161
+ if (!Number.isFinite(z)) {
162
+ counts.skipped++;
163
+ continue;
164
+ }
165
+ const zDir = path_1.default.join(legacyDir, zEntry.name);
166
+ let xEntries;
167
+ try {
168
+ xEntries = await fs_1.promises.readdir(zDir, { withFileTypes: true });
169
+ }
170
+ catch {
171
+ counts.failed++;
172
+ continue;
173
+ }
174
+ for (const xEntry of xEntries) {
175
+ if (opts.signal?.aborted)
176
+ return counts;
177
+ if (!xEntry.isDirectory()) {
178
+ counts.skipped++;
179
+ continue;
180
+ }
181
+ const x = parseInt(xEntry.name, 10);
182
+ if (!Number.isFinite(x)) {
183
+ counts.skipped++;
184
+ continue;
185
+ }
186
+ const xDir = path_1.default.join(zDir, xEntry.name);
187
+ let yEntries;
188
+ try {
189
+ yEntries = await fs_1.promises.readdir(xDir, { withFileTypes: true });
190
+ }
191
+ catch {
192
+ counts.failed++;
193
+ continue;
194
+ }
195
+ for (const yEntry of yEntries) {
196
+ if (opts.signal?.aborted)
197
+ return counts;
198
+ if (!yEntry.isFile() || !TILE_EXTENSION.test(yEntry.name)) {
199
+ counts.skipped++;
200
+ continue;
201
+ }
202
+ const yBase = yEntry.name.replace(TILE_EXTENSION, '');
203
+ const y = parseInt(yBase, 10);
204
+ if (!Number.isFinite(y)) {
205
+ counts.skipped++;
206
+ continue;
207
+ }
208
+ const tilePath = path_1.default.join(xDir, yEntry.name);
209
+ try {
210
+ const buffer = await fs_1.promises.readFile(tilePath);
211
+ await new Promise((resolve, reject) => {
212
+ mbtilesHandle.putTile(z, x, y, buffer, (err) => {
213
+ if (err)
214
+ reject(err);
215
+ else
216
+ resolve();
217
+ });
218
+ });
219
+ counts.migrated++;
220
+ if (opts.deleteSource) {
221
+ try {
222
+ await fs_1.promises.unlink(tilePath);
223
+ }
224
+ catch {
225
+ // Source delete is best-effort; mbtiles already has the tile.
226
+ }
227
+ }
228
+ }
229
+ catch {
230
+ counts.failed++;
231
+ }
232
+ // Periodic progress signal (every 500 processed) so a long-running
233
+ // migration can surface progress to the caller without flooding.
234
+ const total = counts.migrated + counts.skipped + counts.failed;
235
+ if (opts.onProgress && total % 500 === 0)
236
+ opts.onProgress(counts);
237
+ }
238
+ }
239
+ }
240
+ opts.onProgress?.(counts);
241
+ return counts;
242
+ }
243
+ //# sourceMappingURL=cacheLayout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cacheLayout.js","sourceRoot":"","sources":["../src/cacheLayout.ts"],"names":[],"mappings":"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"}