signalk-charts-provider-simple 2.4.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +85 -21
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/download-manager.d.ts +4 -1
- package/dist/utils/download-manager.d.ts.map +1 -1
- package/dist/utils/download-manager.js +390 -168
- package/dist/utils/download-manager.js.map +1 -1
- package/dist/utils/quarantine.d.ts +20 -5
- package/dist/utils/quarantine.d.ts.map +1 -1
- package/dist/utils/quarantine.js +53 -6
- package/dist/utils/quarantine.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,MAAM,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAsHxD,OAAO,KAAK,EACV,aAAa,EAEb,iBAAiB,EAIjB,QAAQ,EAET,MAAM,YAAY,CAAC;AAwBpB,QAAA,MAAM,iBAAiB,GAAI,KAAK,iBAAiB,KAAG,MA49GnD,CAAC;AA4GF,eAAO,MAAM,oBAAoB,GAC/B,KAAK,QAAQ,EACb,UAAU,aAAa,EACvB,GAAG,MAAM,EACT,GAAG,MAAM,EACT,GAAG,MAAM,KACR,IAiDF,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { writeChartPathMarker } from './utils/path-marker.js';
|
|
|
21
21
|
import { getBlankTileReplacement, getOverzoomedTile } from './utils/tile-overzoom.js';
|
|
22
22
|
import { arePairWithinBase, classifyChartDirAccess, hasHostChartsMountEnv, isHostMountPath, isWithinBase, resolveDefaultChartsPath, validateChartName } from './utils/path-safety.js';
|
|
23
23
|
import { parsePluginConfig } from './utils/plugin-config-schema.js';
|
|
24
|
-
import { cleanupQuarantineDir, makeQuarantineDir, promoteQuarantine, sweepStaleQuarantineDirs } from './utils/quarantine.js';
|
|
24
|
+
import { cleanupQuarantineDir, makeQuarantineDir, makeUniqueQuarantineDir, promoteQuarantine, sweepStaleQuarantineDirs } from './utils/quarantine.js';
|
|
25
25
|
import { parseBody, parseShape } from './utils/rest-validation.js';
|
|
26
26
|
import { getAllConversionProgress as getAllRncProgress, getConversionProgress as getRncProgress, initRncConverter, processPilotTar, processRncZip, setConversionFailed as setRncFailed } from './utils/rnc-converter.js';
|
|
27
27
|
import { getAllConversionProgress as getAllS57Progress, getConversionProgress as getS57Progress, initS57Converter, processGshhg, processS57Directory, processS57Zip, processShpBasemap, setConversionFailed as setS57Failed } from './utils/s57-converter.js';
|
|
@@ -32,6 +32,14 @@ import { downloadAndExtractEncs } from './utils/custom-catalog-download.js';
|
|
|
32
32
|
// is what the marker file records so users can confirm the running build.
|
|
33
33
|
import packageJson from '../package.json' with { type: 'json' };
|
|
34
34
|
const pluginVersion = packageJson.version;
|
|
35
|
+
// The startup-global 'job-completed' handler is re-registered on every
|
|
36
|
+
// start() because its closure captures the current chartPath. Track the
|
|
37
|
+
// previous instance (module scope, like downloadManager itself) so
|
|
38
|
+
// re-registration removes exactly that handler — a blanket
|
|
39
|
+
// removeAllListeners('job-completed') would also strip the per-job
|
|
40
|
+
// promotion/cleanup listeners of downloads still running from a prior
|
|
41
|
+
// plugin lifecycle, stranding their files in the quarantine.
|
|
42
|
+
let globalJobCompletedListener = null;
|
|
35
43
|
// Single source of truth lives in container-jobs.ts as PLUGIN_OWNER_ID
|
|
36
44
|
// (used as the `ownerPluginId` label on every runJob call). The plugin
|
|
37
45
|
// id Signal K uses must match exactly so `cleanupOrphanedJobs` reaps
|
|
@@ -403,8 +411,10 @@ const pluginConstructor = (app) => {
|
|
|
403
411
|
}
|
|
404
412
|
}
|
|
405
413
|
startCatalogUpdateChecker();
|
|
406
|
-
|
|
407
|
-
|
|
414
|
+
if (globalJobCompletedListener) {
|
|
415
|
+
downloadManager.removeListener('job-completed', globalJobCompletedListener);
|
|
416
|
+
}
|
|
417
|
+
globalJobCompletedListener = (job) => {
|
|
408
418
|
app.debug(`Download job completed: ${job.id}, extracted files: ${job.extractedFiles.join(', ')}`);
|
|
409
419
|
for (const fileName of job.extractedFiles) {
|
|
410
420
|
const relativePath = path.relative(chartPath, path.join(job.targetDir, fileName));
|
|
@@ -421,7 +431,8 @@ const pluginConstructor = (app) => {
|
|
|
421
431
|
}
|
|
422
432
|
}
|
|
423
433
|
});
|
|
424
|
-
}
|
|
434
|
+
};
|
|
435
|
+
downloadManager.on('job-completed', globalJobCompletedListener);
|
|
425
436
|
// Filesystem housekeeping (removing invalid .mbtiles and orphaned
|
|
426
437
|
// journal/WAL files) runs independently of the display path so it can
|
|
427
438
|
// never delay chart listing. It rescans the chart directory itself
|
|
@@ -616,6 +627,13 @@ const pluginConstructor = (app) => {
|
|
|
616
627
|
.json({ success: false, error: `Invalid chart name: ${chartNameCheck.reason}` });
|
|
617
628
|
return;
|
|
618
629
|
}
|
|
630
|
+
// Setup-failure cleanup state: if anything throws after the
|
|
631
|
+
// quarantine dir exists but before the job owns it, the catch
|
|
632
|
+
// below removes the orphan. Once createJob has returned, the
|
|
633
|
+
// job's completion/failure listeners own the dir — the catch
|
|
634
|
+
// must NOT touch it then.
|
|
635
|
+
let stagedDir = null;
|
|
636
|
+
let createdJobId = null;
|
|
619
637
|
try {
|
|
620
638
|
console.log(`Creating download job for: ${downloadUrl}`);
|
|
621
639
|
console.log(`Target folder: ${targetFolder}`);
|
|
@@ -628,11 +646,16 @@ const pluginConstructor = (app) => {
|
|
|
628
646
|
if (!fs.existsSync(targetDir)) {
|
|
629
647
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
630
648
|
}
|
|
631
|
-
// Stage the download in
|
|
649
|
+
// Stage the download in a per-job quarantine dir; promote on
|
|
632
650
|
// job-completed so a crash mid-fetch never leaves a partial
|
|
633
|
-
// .mbtiles in the live chart library.
|
|
634
|
-
|
|
651
|
+
// .mbtiles in the live chart library. Per-job (unique) because
|
|
652
|
+
// download jobs run concurrently: two downloads with the same
|
|
653
|
+
// chart name must not share a dir, or the first to finish wipes
|
|
654
|
+
// the workspace out from under the other mid-extraction.
|
|
655
|
+
const quarantineDir = makeUniqueQuarantineDir(app.getDataDirPath(), chartName);
|
|
656
|
+
stagedDir = quarantineDir;
|
|
635
657
|
const jobId = downloadManager.createJob(downloadUrl, quarantineDir, chartName);
|
|
658
|
+
createdJobId = jobId;
|
|
636
659
|
const promoteListener = (job) => {
|
|
637
660
|
if (job.id !== jobId) {
|
|
638
661
|
return;
|
|
@@ -688,6 +711,9 @@ const pluginConstructor = (app) => {
|
|
|
688
711
|
});
|
|
689
712
|
}
|
|
690
713
|
catch (error) {
|
|
714
|
+
if (stagedDir !== null && createdJobId === null) {
|
|
715
|
+
cleanupQuarantineDir(stagedDir);
|
|
716
|
+
}
|
|
691
717
|
console.error('Error creating download job:', error);
|
|
692
718
|
res.status(500).json({
|
|
693
719
|
success: false,
|
|
@@ -1326,7 +1352,7 @@ const pluginConstructor = (app) => {
|
|
|
1326
1352
|
// streamed here first, then atomically promoted on `finish`.
|
|
1327
1353
|
// A crashed/aborted request leaves the partial files in the
|
|
1328
1354
|
// quarantine for the startup sweep to wipe, never in basePath.
|
|
1329
|
-
const quarantineDir =
|
|
1355
|
+
const quarantineDir = makeUniqueQuarantineDir(app.getDataDirPath(), 'upload');
|
|
1330
1356
|
const uploadedFiles = [];
|
|
1331
1357
|
const writePromises = [];
|
|
1332
1358
|
const fields = {};
|
|
@@ -1371,6 +1397,11 @@ const pluginConstructor = (app) => {
|
|
|
1371
1397
|
reject(err);
|
|
1372
1398
|
});
|
|
1373
1399
|
});
|
|
1400
|
+
// Observe the rejection now: Promise.all only attaches in the
|
|
1401
|
+
// 'finish' handler, which never fires on an aborted request —
|
|
1402
|
+
// a staging failure there would otherwise surface as a
|
|
1403
|
+
// process-level unhandled rejection.
|
|
1404
|
+
writePromise.catch(() => undefined);
|
|
1374
1405
|
writePromises.push(writePromise);
|
|
1375
1406
|
});
|
|
1376
1407
|
bb.on('finish', () => {
|
|
@@ -1433,6 +1464,23 @@ const pluginConstructor = (app) => {
|
|
|
1433
1464
|
}
|
|
1434
1465
|
})();
|
|
1435
1466
|
});
|
|
1467
|
+
// An aborted request or a Busboy parse error never reaches
|
|
1468
|
+
// 'finish', which owns the normal cleanup — without these the
|
|
1469
|
+
// staged files stay on disk and the dir stays registered as
|
|
1470
|
+
// active (so the startup sweep skips it) until a full server
|
|
1471
|
+
// restart. cleanupQuarantineDir is idempotent, so a late abort
|
|
1472
|
+
// racing the finish path is harmless.
|
|
1473
|
+
bb.on('error', (err) => {
|
|
1474
|
+
app.error('Upload stream error: ' + (err instanceof Error ? err.message : String(err)));
|
|
1475
|
+
cleanupQuarantineDir(quarantineDir);
|
|
1476
|
+
if (!res.headersSent) {
|
|
1477
|
+
res.status(400).send('Malformed upload');
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
req.on('aborted', () => {
|
|
1481
|
+
app.debug('Upload request aborted by client');
|
|
1482
|
+
cleanupQuarantineDir(quarantineDir);
|
|
1483
|
+
});
|
|
1436
1484
|
req.pipe(bb);
|
|
1437
1485
|
}
|
|
1438
1486
|
catch (error) {
|
|
@@ -1728,7 +1776,7 @@ const pluginConstructor = (app) => {
|
|
|
1728
1776
|
// the UI stuck waiting for status it would never get.
|
|
1729
1777
|
let quarantineDir;
|
|
1730
1778
|
try {
|
|
1731
|
-
quarantineDir =
|
|
1779
|
+
quarantineDir = makeUniqueQuarantineDir(app.getDataDirPath(), chartNumber);
|
|
1732
1780
|
}
|
|
1733
1781
|
catch (err) {
|
|
1734
1782
|
res.status(500).json({
|
|
@@ -1820,6 +1868,11 @@ const pluginConstructor = (app) => {
|
|
|
1820
1868
|
});
|
|
1821
1869
|
return;
|
|
1822
1870
|
}
|
|
1871
|
+
// Setup-failure cleanup state for the direct-.mbtiles branch: see
|
|
1872
|
+
// the download-chart-locker route. Once createJob has returned,
|
|
1873
|
+
// the job's listeners own the dir and the catch must leave it be.
|
|
1874
|
+
let stagedDir = null;
|
|
1875
|
+
let createdJobId = null;
|
|
1823
1876
|
try {
|
|
1824
1877
|
const chartPath = props.chartPath || defaultChartsPath;
|
|
1825
1878
|
const targetDir = !targetFolder || targetFolder === '/' ? chartPath : path.join(chartPath, targetFolder);
|
|
@@ -1876,13 +1929,16 @@ const pluginConstructor = (app) => {
|
|
|
1876
1929
|
handleGshhgDownload(res, app, chartNumber, catalogFile, zipfileDatetime, url, targetDir, chartPath);
|
|
1877
1930
|
}
|
|
1878
1931
|
else {
|
|
1879
|
-
// Direct .mbtiles download (or ZIP-of-.mbtiles): stage into
|
|
1880
|
-
// quarantine dir so a crash mid-download/extract never
|
|
1881
|
-
// half-built file in the live chart library
|
|
1882
|
-
//
|
|
1883
|
-
|
|
1932
|
+
// Direct .mbtiles download (or ZIP-of-.mbtiles): stage into a
|
|
1933
|
+
// per-job quarantine dir so a crash mid-download/extract never
|
|
1934
|
+
// leaves a half-built file in the live chart library, and so a
|
|
1935
|
+
// re-download of the same chartNumber can't share (and wipe)
|
|
1936
|
+
// a still-running job's dir. Promotion runs on job-completed;
|
|
1937
|
+
// failure / no-files paths drop the quarantine.
|
|
1938
|
+
const quarantineDir = makeUniqueQuarantineDir(app.getDataDirPath(), chartNumber);
|
|
1939
|
+
stagedDir = quarantineDir;
|
|
1884
1940
|
const jobId = downloadManager.createJob(url, quarantineDir, chartNumber);
|
|
1885
|
-
|
|
1941
|
+
createdJobId = jobId;
|
|
1886
1942
|
const cleanupListener = (job) => {
|
|
1887
1943
|
if (job.id !== jobId) {
|
|
1888
1944
|
return;
|
|
@@ -1951,6 +2007,11 @@ const pluginConstructor = (app) => {
|
|
|
1951
2007
|
};
|
|
1952
2008
|
downloadManager.on('job-failed', cleanupListener);
|
|
1953
2009
|
downloadManager.on('job-completed', cleanupListener);
|
|
2010
|
+
// Only after the listeners own the job/quarantine lifecycle:
|
|
2011
|
+
// if this throws, the catch below must NOT touch the dir (the
|
|
2012
|
+
// listeners will promote or clean it), and nothing before the
|
|
2013
|
+
// registration above can throw once createJob has returned.
|
|
2014
|
+
trackInstall(chartNumber, catalogFile, zipfileDatetime ?? '', url);
|
|
1954
2015
|
app.debug(`Catalog download started: ${chartNumber} from ${catalogFile}, job: ${jobId}`);
|
|
1955
2016
|
res.json({
|
|
1956
2017
|
success: true,
|
|
@@ -1960,6 +2021,9 @@ const pluginConstructor = (app) => {
|
|
|
1960
2021
|
}
|
|
1961
2022
|
}
|
|
1962
2023
|
catch (error) {
|
|
2024
|
+
if (stagedDir !== null && createdJobId === null) {
|
|
2025
|
+
cleanupQuarantineDir(stagedDir);
|
|
2026
|
+
}
|
|
1963
2027
|
console.error('Error creating catalog download job:', error);
|
|
1964
2028
|
res.status(500).json({
|
|
1965
2029
|
success: false,
|
|
@@ -2205,7 +2269,7 @@ const pluginConstructor = (app) => {
|
|
|
2205
2269
|
// the same cleanup as a conversion failure.
|
|
2206
2270
|
let quarantineDir = null;
|
|
2207
2271
|
try {
|
|
2208
|
-
quarantineDir =
|
|
2272
|
+
quarantineDir = makeUniqueQuarantineDir(app.getDataDirPath(), chartNumber);
|
|
2209
2273
|
const displayName = chartTitle ? cleanCatalogTitle(chartTitle) : undefined;
|
|
2210
2274
|
const result = await processS57Zip(zipPath, quarantineDir, chartNumber, (status, message) => {
|
|
2211
2275
|
app.debug(`S-57 [${chartNumber}] ${status}: ${message}`);
|
|
@@ -2288,7 +2352,7 @@ const pluginConstructor = (app) => {
|
|
|
2288
2352
|
app.debug(`Starting RNC conversion for ${chartNumber}: ${zipPath}`);
|
|
2289
2353
|
let quarantineDir = null;
|
|
2290
2354
|
try {
|
|
2291
|
-
quarantineDir =
|
|
2355
|
+
quarantineDir = makeUniqueQuarantineDir(app.getDataDirPath(), chartNumber);
|
|
2292
2356
|
const result = await processRncZip(zipPath, quarantineDir, chartNumber, (status, message) => {
|
|
2293
2357
|
app.debug(`RNC [${chartNumber}] ${status}: ${message}`);
|
|
2294
2358
|
});
|
|
@@ -2369,7 +2433,7 @@ const pluginConstructor = (app) => {
|
|
|
2369
2433
|
setConvertingState(chartNumber, true);
|
|
2370
2434
|
let quarantineDir = null;
|
|
2371
2435
|
try {
|
|
2372
|
-
quarantineDir =
|
|
2436
|
+
quarantineDir = makeUniqueQuarantineDir(app.getDataDirPath(), chartNumber);
|
|
2373
2437
|
const result = await processPilotTar(dlPath, quarantineDir, chartNumber, (status, message) => {
|
|
2374
2438
|
app.debug(`Pilot [${chartNumber}] ${status}: ${message}`);
|
|
2375
2439
|
});
|
|
@@ -2440,7 +2504,7 @@ const pluginConstructor = (app) => {
|
|
|
2440
2504
|
setConvertingState(chartNumber, true);
|
|
2441
2505
|
let quarantineDir = null;
|
|
2442
2506
|
try {
|
|
2443
|
-
quarantineDir =
|
|
2507
|
+
quarantineDir = makeUniqueQuarantineDir(app.getDataDirPath(), chartNumber);
|
|
2444
2508
|
const result = await processShpBasemap(dlPath, quarantineDir, chartNumber, (status, message) => {
|
|
2445
2509
|
app.debug(`ShpBasemap [${chartNumber}] ${status}: ${message}`);
|
|
2446
2510
|
});
|
|
@@ -2491,7 +2555,7 @@ const pluginConstructor = (app) => {
|
|
|
2491
2555
|
void (async () => {
|
|
2492
2556
|
let quarantineDir = null;
|
|
2493
2557
|
try {
|
|
2494
|
-
quarantineDir =
|
|
2558
|
+
quarantineDir = makeUniqueQuarantineDir(app.getDataDirPath(), chartNumber);
|
|
2495
2559
|
const result = await processGshhg(tmpDir, quarantineDir, resolution, chartNumber, (status, message) => {
|
|
2496
2560
|
app.debug(`GSHHG [${chartNumber}] ${status}: ${message}`);
|
|
2497
2561
|
});
|
|
@@ -2673,7 +2737,7 @@ const pluginConstructor = (app) => {
|
|
|
2673
2737
|
}
|
|
2674
2738
|
setConvertingState(id, true);
|
|
2675
2739
|
convertingFlagged = true;
|
|
2676
|
-
quarantineDir =
|
|
2740
|
+
quarantineDir = makeUniqueQuarantineDir(app.getDataDirPath(), id);
|
|
2677
2741
|
const conv = await processS57Directory(encDir, quarantineDir, id, (status, message) => setCatalogProgress(id, status, message), {
|
|
2678
2742
|
displayName: catalog.name,
|
|
2679
2743
|
displayDescription: `Custom catalog: ${catalog.name}`,
|