pmtiles-swarm 0.65.0 → 0.66.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/CHANGELOG.md +25 -0
- package/docs/tile-stacks.md +14 -0
- package/package.json +1 -1
- package/src/api.js +2 -0
- package/src/bake-jobs.js +34 -8
- package/src/bake.js +6 -2
- package/src/web/index.html +139 -21
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@
|
|
|
4
4
|
### ✨ Features and improvements
|
|
5
5
|
- _...Add new stuff here..._
|
|
6
6
|
|
|
7
|
+
### 🐞 Bug fixes
|
|
8
|
+
- _...Add new stuff here..._
|
|
9
|
+
|
|
10
|
+
## 0.66.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **An export asks where it should go, and what to call it.** **Export to archive** now opens a
|
|
13
|
+
dialog instead of a confirmation, because there is something to decide. The save location uses
|
|
14
|
+
the same picker every other destination in this console uses, so a location named once under
|
|
15
|
+
Settings is offered here too — a baked planet is hundreds of gigabytes and which disk it lands on
|
|
16
|
+
is not a detail. The name and description default to the stack's and can be changed.
|
|
17
|
+
|
|
18
|
+
The filename is shown as the name is typed. A name is not a filename — separators and the
|
|
19
|
+
characters Windows refuses become dashes, and the date is appended so successive builds do not
|
|
20
|
+
collide — and a preview promising one thing while the server writes another would be worse than
|
|
21
|
+
no preview at all. A test asserts the browser's rule and `safeSegment` agree.
|
|
22
|
+
|
|
23
|
+
### 🐞 Bug fixes
|
|
24
|
+
- **A save location the node did not know was silently ignored.** The export resolved it after the
|
|
25
|
+
merge and swallowed the error, so naming a location that did not exist, or one that could not be
|
|
26
|
+
written, meant an hour of merging answered with a shrug and the archive on the default disk. It
|
|
27
|
+
is resolved before anything starts and the error is allowed through, which is what lets the
|
|
28
|
+
dialog put it beside the field that caused it.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
7
32
|
### 🐞 Bug fixes
|
|
8
33
|
- _...Add new stuff here..._
|
|
9
34
|
|
package/docs/tile-stacks.md
CHANGED
|
@@ -730,6 +730,20 @@ worth writing at all.
|
|
|
730
730
|
planet bake is hours, and a request that waited for the archive is a request
|
|
731
731
|
nothing could hold open. `DELETE` on the same address stops it.
|
|
732
732
|
|
|
733
|
+
The button opens a dialog rather than a confirmation, because there is
|
|
734
|
+
something to decide. **Where it lands** uses the same picker every other
|
|
735
|
+
destination in this console uses, so a location named once under Settings is
|
|
736
|
+
offered here too — and a baked planet is hundreds of gigabytes, so which disk it
|
|
737
|
+
goes on is not a detail. The location is resolved _before_ anything is merged:
|
|
738
|
+
a name this node does not know, or a path it cannot write, is the caller's
|
|
739
|
+
mistake and they can fix it, but only if they are told now rather than an hour
|
|
740
|
+
later.
|
|
741
|
+
|
|
742
|
+
**What it is called** defaults to the stack's title and can be changed, as can
|
|
743
|
+
the description. The dialog shows the filename as the name is typed, because a
|
|
744
|
+
name is not a filename — the same sanitising the server does, and a preview that
|
|
745
|
+
promised something else would be worse than no preview.
|
|
746
|
+
|
|
733
747
|
A bake has two halves and they are watched in two places, deliberately. Merging
|
|
734
748
|
is about a stack, so it is reported on the stack — tiles written, tiles skipped,
|
|
735
749
|
the zoom it is working through. What happens afterwards is an archive being
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.0",
|
|
4
4
|
"description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/api.js
CHANGED
|
@@ -3181,6 +3181,8 @@ export function createApp({
|
|
|
3181
3181
|
categories: req.body?.categories,
|
|
3182
3182
|
location: req.body?.location,
|
|
3183
3183
|
savePath: req.body?.savePath,
|
|
3184
|
+
name: req.body?.name,
|
|
3185
|
+
description: req.body?.description,
|
|
3184
3186
|
});
|
|
3185
3187
|
return res.status(202).json({ bake: job });
|
|
3186
3188
|
} catch (error) {
|
package/src/bake-jobs.js
CHANGED
|
@@ -113,9 +113,21 @@ export class BakeManager {
|
|
|
113
113
|
throw error;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// Resolved before anything starts, and allowed to throw. A location that
|
|
117
|
+
// does not exist or cannot be written is the caller's mistake and they can
|
|
118
|
+
// fix it -- but only if they are told now. Checked at the end instead, it
|
|
119
|
+
// is an hour of merging answered with a shrug and the default disk.
|
|
120
|
+
const publishDir =
|
|
121
|
+
options.publishDir ?? (await this.#savePath(options)) ?? undefined;
|
|
122
|
+
|
|
123
|
+
// What the archive will be called, which the caller may have chosen. The
|
|
124
|
+
// file follows it; the date is what keeps successive builds apart.
|
|
125
|
+
const archiveName = options.name?.trim() || resolved.stack.title || stackId;
|
|
126
|
+
|
|
116
127
|
const job = {
|
|
117
128
|
stackId,
|
|
118
129
|
title: resolved.stack.title ?? stackId,
|
|
130
|
+
archiveName,
|
|
119
131
|
phase: 'merging',
|
|
120
132
|
startedAt: new Date().toISOString(),
|
|
121
133
|
written: 0,
|
|
@@ -126,7 +138,8 @@ export class BakeManager {
|
|
|
126
138
|
finishedAt: null,
|
|
127
139
|
error: null,
|
|
128
140
|
infoHash: null,
|
|
129
|
-
name: bakedName(resolved, { when: new Date() }),
|
|
141
|
+
name: bakedName(resolved, { when: new Date(), name: archiveName }),
|
|
142
|
+
publishDir,
|
|
130
143
|
};
|
|
131
144
|
this.#jobs.set(stackId, job);
|
|
132
145
|
|
|
@@ -171,6 +184,7 @@ export class BakeManager {
|
|
|
171
184
|
workDir,
|
|
172
185
|
destination,
|
|
173
186
|
format,
|
|
187
|
+
options,
|
|
174
188
|
);
|
|
175
189
|
} finally {
|
|
176
190
|
await pixels?.close().catch(() => {});
|
|
@@ -185,7 +199,7 @@ export class BakeManager {
|
|
|
185
199
|
// Moved out of the working directory as it is taken on, so a finished
|
|
186
200
|
// archive does not live among the checkpoint files of the job that made
|
|
187
201
|
// it.
|
|
188
|
-
publishDir:
|
|
202
|
+
publishDir: job.publishDir,
|
|
189
203
|
mode: 'mirror',
|
|
190
204
|
});
|
|
191
205
|
|
|
@@ -205,7 +219,16 @@ export class BakeManager {
|
|
|
205
219
|
* @param {string} format - The output format.
|
|
206
220
|
* @returns {Promise<void>} - Resolves when the file is written.
|
|
207
221
|
*/
|
|
208
|
-
async #merge(
|
|
222
|
+
async #merge(
|
|
223
|
+
job,
|
|
224
|
+
resolved,
|
|
225
|
+
codec,
|
|
226
|
+
pixels,
|
|
227
|
+
workDir,
|
|
228
|
+
destination,
|
|
229
|
+
format,
|
|
230
|
+
options,
|
|
231
|
+
) {
|
|
209
232
|
// Read through the tile store rather than off the disk, so a cache-mode
|
|
210
233
|
// source is scanned the same way it is served: its directories come out of
|
|
211
234
|
// the swarm, and the store holds them to its own byte budget.
|
|
@@ -249,8 +272,8 @@ export class BakeManager {
|
|
|
249
272
|
header: { format },
|
|
250
273
|
pauseMs: this.#config.stacks?.bakePauseMs ?? 0,
|
|
251
274
|
metadata: {
|
|
252
|
-
name:
|
|
253
|
-
description: resolved.stack.description,
|
|
275
|
+
name: job.archiveName,
|
|
276
|
+
description: options.description ?? resolved.stack.description,
|
|
254
277
|
attribution: resolved.stack.attribution,
|
|
255
278
|
encoding: resolved.stack.output?.encoding,
|
|
256
279
|
encodingFactors: resolved.stack.output,
|
|
@@ -273,9 +296,10 @@ export class BakeManager {
|
|
|
273
296
|
* @returns {Promise<string|undefined>} - A path, or undefined for the default.
|
|
274
297
|
*/
|
|
275
298
|
async #savePath(options) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
299
|
+
// Not caught. `resolveSavePath` refuses a name this node does not know and
|
|
300
|
+
// a path it cannot write, and both are worth refusing rather than quietly
|
|
301
|
+
// putting hundreds of gigabytes somewhere else.
|
|
302
|
+
const explicit = await this.#library.resolveSavePath(options);
|
|
279
303
|
return explicit ?? this.#config.savePath ?? undefined;
|
|
280
304
|
}
|
|
281
305
|
|
|
@@ -295,6 +319,8 @@ export class BakeManager {
|
|
|
295
319
|
skipped: job.skipped,
|
|
296
320
|
zoom: job.zoom,
|
|
297
321
|
name: job.name,
|
|
322
|
+
archiveName: job.archiveName,
|
|
323
|
+
publishDir: job.publishDir ?? null,
|
|
298
324
|
infoHash: job.infoHash,
|
|
299
325
|
error: job.error,
|
|
300
326
|
};
|
package/src/bake.js
CHANGED
|
@@ -232,12 +232,16 @@ function stamp(when = new Date()) {
|
|
|
232
232
|
* map and two files cannot share a path. The archive's *name* is not dated --
|
|
233
233
|
* see `bakedMetadata` -- so a rebuild keeps its identity the way every other
|
|
234
234
|
* rebuild here does.
|
|
235
|
+
*
|
|
236
|
+
* Follows the name it is given, so renaming an export in the dialog renames the
|
|
237
|
+
* file too. A name and a filename that disagree is a small thing that costs an
|
|
238
|
+
* afternoon the first time somebody goes looking on disk.
|
|
235
239
|
* @param {object} resolved - The resolved stack.
|
|
236
|
-
* @param {object} [options] - `
|
|
240
|
+
* @param {object} [options] - `name` to override the stack's, `when`, `suffix`.
|
|
237
241
|
* @returns {string} - A filename.
|
|
238
242
|
*/
|
|
239
243
|
export function bakedName(resolved, options = {}) {
|
|
240
|
-
const title = resolved.stack.title ?? resolved.stack.id;
|
|
244
|
+
const title = options.name ?? resolved.stack.title ?? resolved.stack.id;
|
|
241
245
|
const slug = safeSegment(title) || 'stack';
|
|
242
246
|
const tail = options.suffix ? `-${options.suffix}` : '';
|
|
243
247
|
return `${slug}-${stamp(options.when)}${tail}.pmtiles`;
|
package/src/web/index.html
CHANGED
|
@@ -644,6 +644,59 @@
|
|
|
644
644
|
</form>
|
|
645
645
|
</dialog>
|
|
646
646
|
|
|
647
|
+
<dialog id="bake-dialog">
|
|
648
|
+
<form method="dialog" id="bake-form">
|
|
649
|
+
<h2>Export to archive</h2>
|
|
650
|
+
<div class="sub" style="margin-bottom:0.8rem">
|
|
651
|
+
Runs the recipe over its sources and writes the result as a real
|
|
652
|
+
archive, with its own infohash, seeded like any other. It reads every
|
|
653
|
+
tile the sources hold, so it can take hours — and it can be stopped,
|
|
654
|
+
which keeps the work for the next run to carry on from.
|
|
655
|
+
</div>
|
|
656
|
+
|
|
657
|
+
<div class="field">
|
|
658
|
+
<label>Stack</label>
|
|
659
|
+
<div class="sub"><code id="bake-stack"></code></div>
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
<div class="field">
|
|
663
|
+
<label for="bake-name">Name</label>
|
|
664
|
+
<input id="bake-name" placeholder="Terrain" />
|
|
665
|
+
<div class="sub">
|
|
666
|
+
What the archive is called, and what a rebuild of it keeps — so
|
|
667
|
+
leave the date out of it. The file gets one:
|
|
668
|
+
<code id="bake-filename"></code>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
<div class="field">
|
|
673
|
+
<label for="bake-description">Description</label>
|
|
674
|
+
<input id="bake-description" placeholder="Bathymetry under terrain" />
|
|
675
|
+
<div class="sub">
|
|
676
|
+
Optional. The date this was baked is added to whatever is here.
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
<div id="bake-location"></div>
|
|
681
|
+
|
|
682
|
+
<div class="field">
|
|
683
|
+
<label for="bake-categories">Categories</label>
|
|
684
|
+
<input id="bake-categories" placeholder="terrain, weekly" />
|
|
685
|
+
<div class="sub">
|
|
686
|
+
Comma separated. What the archive is filed under once it is added,
|
|
687
|
+
which is what <code>/latest/<category>/</code> follows. Left
|
|
688
|
+
empty, it takes the stack's own.
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<div class="sub" id="bake-error" style="color: var(--danger)"></div>
|
|
693
|
+
<menu>
|
|
694
|
+
<button value="cancel">Cancel</button>
|
|
695
|
+
<button id="bake-go" value="default">Export</button>
|
|
696
|
+
</menu>
|
|
697
|
+
</form>
|
|
698
|
+
</dialog>
|
|
699
|
+
|
|
647
700
|
<dialog id="adopt-dialog">
|
|
648
701
|
<form method="dialog" id="adopt-form">
|
|
649
702
|
<h2>Adopt existing torrents</h2>
|
|
@@ -7678,6 +7731,87 @@ Every piece is hashed against the ` +
|
|
|
7678
7731
|
$('stacks-add').onclick = () =>
|
|
7679
7732
|
openStackDialog().catch((e) => toast(e.message));
|
|
7680
7733
|
|
|
7734
|
+
/**
|
|
7735
|
+
* Asks where an export should land before starting it.
|
|
7736
|
+
*
|
|
7737
|
+
* A dialog rather than a confirm, because there is something to decide:
|
|
7738
|
+
* a baked planet is hundreds of gigabytes and the disk it goes on is not
|
|
7739
|
+
* a detail. The same picker every other destination in this console
|
|
7740
|
+
* uses, so a location named once under Settings is offered here too.
|
|
7741
|
+
* @param {string} id - The stack to export.
|
|
7742
|
+
* @returns {Promise<void>} - Resolves once it is asked for or dismissed.
|
|
7743
|
+
*/
|
|
7744
|
+
async function openBakeDialog(id, stack) {
|
|
7745
|
+
$('bake-stack').textContent = id;
|
|
7746
|
+
$('bake-error').textContent = '';
|
|
7747
|
+
$('bake-name').value = stack?.title ?? id;
|
|
7748
|
+
$('bake-description').value = stack?.description ?? '';
|
|
7749
|
+
$('bake-categories').value = (stack?.categories ?? []).join(', ');
|
|
7750
|
+
showBakeFilename();
|
|
7751
|
+
$('bake-location').innerHTML = locationPicker('bake-loc');
|
|
7752
|
+
await fillLocations('bake-loc');
|
|
7753
|
+
$('bake-dialog').showModal();
|
|
7754
|
+
}
|
|
7755
|
+
|
|
7756
|
+
/**
|
|
7757
|
+
* Shows what the file will be called, as the name is typed.
|
|
7758
|
+
*
|
|
7759
|
+
* The same rules the server applies, because a name is not a filename:
|
|
7760
|
+
* separators and the characters Windows refuses become dashes, and the
|
|
7761
|
+
* date is appended so successive builds do not collide. Seeing that
|
|
7762
|
+
* before pressing the button beats finding it on disk afterwards.
|
|
7763
|
+
* @returns {void}
|
|
7764
|
+
*/
|
|
7765
|
+
function showBakeFilename() {
|
|
7766
|
+
const typed = $('bake-name').value.trim() || $('bake-stack').textContent;
|
|
7767
|
+
const slug =
|
|
7768
|
+
typed
|
|
7769
|
+
.replace(/[/\\<>:"|?*\u0000-\u001f\u007f -]/g, '-')
|
|
7770
|
+
.replace(/-{2,}/g, '-')
|
|
7771
|
+
.replace(/^[.\s-]+/, '')
|
|
7772
|
+
.replace(/[.\s-]+$/, '')
|
|
7773
|
+
.slice(0, 120) || 'stack';
|
|
7774
|
+
const day = new Date().toISOString().slice(0, 10).replaceAll('-', '');
|
|
7775
|
+
$('bake-filename').textContent = `${slug}-${day}.pmtiles`;
|
|
7776
|
+
}
|
|
7777
|
+
|
|
7778
|
+
$('bake-name').addEventListener('input', showBakeFilename);
|
|
7779
|
+
|
|
7780
|
+
$('bake-form').addEventListener('submit', async (event) => {
|
|
7781
|
+
if (event.submitter?.value === 'cancel') return;
|
|
7782
|
+
event.preventDefault();
|
|
7783
|
+
|
|
7784
|
+
const id = $('bake-stack').textContent;
|
|
7785
|
+
const categories = $('bake-categories')
|
|
7786
|
+
.value.split(',')
|
|
7787
|
+
.map((one) => one.trim())
|
|
7788
|
+
.filter(Boolean);
|
|
7789
|
+
const name = $('bake-name').value.trim();
|
|
7790
|
+
const description = $('bake-description').value.trim();
|
|
7791
|
+
|
|
7792
|
+
try {
|
|
7793
|
+
await api(`/api/stacks/${encodeURIComponent(id)}/bake`, {
|
|
7794
|
+
method: 'POST',
|
|
7795
|
+
body: {
|
|
7796
|
+
...chosenLocation('bake-loc'),
|
|
7797
|
+
...(categories.length > 0 ? { categories } : {}),
|
|
7798
|
+
...(name ? { name } : {}),
|
|
7799
|
+
...(description ? { description } : {}),
|
|
7800
|
+
},
|
|
7801
|
+
});
|
|
7802
|
+
} catch (error) {
|
|
7803
|
+
// Kept open, because the answer is usually something to change here
|
|
7804
|
+
// -- a location that does not exist, or a codec that is not
|
|
7805
|
+
// installed -- rather than something to go away and think about.
|
|
7806
|
+
$('bake-error').textContent = error.message;
|
|
7807
|
+
return;
|
|
7808
|
+
}
|
|
7809
|
+
|
|
7810
|
+
$('bake-dialog').close();
|
|
7811
|
+
toast(`Exporting ${id}`);
|
|
7812
|
+
loadStacks().catch((e) => toast(e.message));
|
|
7813
|
+
});
|
|
7814
|
+
|
|
7681
7815
|
// Editing and deleting hang off the list, which is redrawn on every
|
|
7682
7816
|
// change, so they are delegated the same way the source rows are.
|
|
7683
7817
|
$('stacks-list').addEventListener('click', async (event) => {
|
|
@@ -7686,27 +7820,11 @@ Every piece is hashed against the ` +
|
|
|
7686
7820
|
const bake = event.target.dataset.stackBake;
|
|
7687
7821
|
const stopBake = event.target.dataset.stackBakeStop;
|
|
7688
7822
|
if (bake !== undefined) {
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
'archive?\n\nThis reads every tile the sources hold and can ' +
|
|
7695
|
-
'take hours. It can be stopped, and stopping keeps the work.',
|
|
7696
|
-
)
|
|
7697
|
-
) {
|
|
7698
|
-
return;
|
|
7699
|
-
}
|
|
7700
|
-
try {
|
|
7701
|
-
await api(`/api/stacks/${encodeURIComponent(bake)}/bake`, {
|
|
7702
|
-
method: 'POST',
|
|
7703
|
-
body: {},
|
|
7704
|
-
});
|
|
7705
|
-
toast(`Exporting ${bake}`);
|
|
7706
|
-
} catch (error) {
|
|
7707
|
-
toast(error.message);
|
|
7708
|
-
}
|
|
7709
|
-
loadStacks().catch((e) => toast(e.message));
|
|
7823
|
+
const { stacks } = await api('/api/stacks');
|
|
7824
|
+
await openBakeDialog(
|
|
7825
|
+
bake,
|
|
7826
|
+
stacks.find((one) => one.id === bake),
|
|
7827
|
+
).catch((e) => toast(e.message));
|
|
7710
7828
|
} else if (stopBake !== undefined) {
|
|
7711
7829
|
try {
|
|
7712
7830
|
await api(`/api/stacks/${encodeURIComponent(stopBake)}/bake`, {
|