pmtiles-swarm 0.66.0 → 0.67.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 +31 -0
- package/docs/tile-stacks.md +23 -10
- package/package.json +1 -1
- package/src/bake-jobs.js +15 -5
- package/src/bake.js +34 -12
- package/src/web/index.html +60 -24
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@
|
|
|
4
4
|
### ✨ Features and improvements
|
|
5
5
|
- _...Add new stuff here..._
|
|
6
6
|
|
|
7
|
+
### 🐞 Bug fixes
|
|
8
|
+
- _...Add new stuff here..._
|
|
9
|
+
|
|
10
|
+
## 0.67.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **An exported archive is dated, and the filename is its own field.** Both the archive's name and
|
|
13
|
+
the file it lands in now carry the date by default, and both can be changed — separately. They
|
|
14
|
+
answer different questions: `Terrain-20260822.pmtiles` is what somebody finds on disk,
|
|
15
|
+
`Terrain 20260822` is what a map client shows, and tying one to the other only guarantees that
|
|
16
|
+
one of them is wrong whenever they should differ.
|
|
17
|
+
|
|
18
|
+
A filename given by hand is reduced to a single path segment before it is used, because it is
|
|
19
|
+
joined to a save path and a filename is exactly the kind of field somebody puts a slash in.
|
|
20
|
+
`../../etc/passwd` is tested.
|
|
21
|
+
|
|
22
|
+
The description starts empty and stays empty unless something is typed. It used to be
|
|
23
|
+
prefilled from the recipe, which is a different thing — a recipe describes how tiles are
|
|
24
|
+
combined, an archive describes what it is, and only the person exporting it knows that. The
|
|
25
|
+
server no longer falls back to the recipe either: filling in a field the dialog showed as
|
|
26
|
+
blank is a worse surprise than having no description. The date is recorded there regardless,
|
|
27
|
+
because a name can be changed to anything and then nothing else says when the archive was
|
|
28
|
+
made.
|
|
29
|
+
|
|
30
|
+
This corrects something 0.64.0 asserted and this project does not do. The name was left undated
|
|
31
|
+
on the reasoning that `/latest/<category>/` follows a rebuild by name. It does not: it resolves a
|
|
32
|
+
category and takes the newest by date, and nothing here looks an archive up by name at all — the
|
|
33
|
+
only name comparison in the codebase refuses two archives the same *file* path. The
|
|
34
|
+
documentation said so as well, and now says what is true.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
7
38
|
### 🐞 Bug fixes
|
|
8
39
|
- _...Add new stuff here..._
|
|
9
40
|
|
package/docs/tile-stacks.md
CHANGED
|
@@ -698,14 +698,26 @@ resolves to whichever build is current, so the same recipe over a rebuilt source
|
|
|
698
698
|
is a different bake — and a checkpoint that could not tell would resume across
|
|
699
699
|
the change and produce an archive that is half one map and half another.
|
|
700
700
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
701
|
+
Both the file and the archive's name are dated, and both can be changed —
|
|
702
|
+
separately, because they answer different questions. `Terrain-20260822.pmtiles`
|
|
703
|
+
is what somebody finds on disk; `Terrain 20260822` is what a map client shows.
|
|
704
|
+
Tying one to the other only guarantees that one of them is wrong whenever they
|
|
705
|
+
should differ.
|
|
706
|
+
|
|
707
|
+
An earlier version of this document said the name had to stay undated so
|
|
708
|
+
`/latest/<category>/` could follow a rebuild. That was wrong. `/latest/`
|
|
709
|
+
resolves a category and takes the newest by date; nothing in this project looks
|
|
710
|
+
an archive up by name at all, and the only name comparison there is refuses two
|
|
711
|
+
archives the same _file_ path. A dated name is free, and it answers the question
|
|
712
|
+
somebody holding two builds actually has.
|
|
706
713
|
|
|
707
714
|
`name` is always written, because these archives get converted to mbtiles by
|
|
708
|
-
other tools and a nameless metadata block is not valid there.
|
|
715
|
+
other tools and a nameless metadata block is not valid there. The date also goes
|
|
716
|
+
in `description`, so an archive read out of context says what produced it.
|
|
717
|
+
|
|
718
|
+
A chosen filename is reduced to one path segment before it is used. That is not
|
|
719
|
+
politeness — the name is joined to a save path, and a filename is exactly the
|
|
720
|
+
kind of field somebody puts a slash in.
|
|
709
721
|
|
|
710
722
|
### What a baked archive says about itself
|
|
711
723
|
|
|
@@ -739,10 +751,11 @@ a name this node does not know, or a path it cannot write, is the caller's
|
|
|
739
751
|
mistake and they can fix it, but only if they are told now rather than an hour
|
|
740
752
|
later.
|
|
741
753
|
|
|
742
|
-
**What it is called**
|
|
743
|
-
the
|
|
744
|
-
|
|
745
|
-
|
|
754
|
+
**What it is called** is two fields, both dated by default and both editable:
|
|
755
|
+
the archive's name, and the filename. The dialog says what a filename will
|
|
756
|
+
actually become where sanitising would change it, using the same rule the server
|
|
757
|
+
applies — a field that shows one filename while the server writes another is
|
|
758
|
+
worse than a field that shows nothing.
|
|
746
759
|
|
|
747
760
|
A bake has two halves and they are watched in two places, deliberately. Merging
|
|
748
761
|
is about a stack, so it is reported on the stack — tiles written, tiles skipped,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.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/bake-jobs.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
assertBakeable,
|
|
4
4
|
bakeRevision,
|
|
5
5
|
bakeStack,
|
|
6
|
+
bakedArchiveName,
|
|
6
7
|
bakedName,
|
|
7
8
|
mergeTileFor,
|
|
8
9
|
} from './bake.js';
|
|
@@ -120,9 +121,15 @@ export class BakeManager {
|
|
|
120
121
|
const publishDir =
|
|
121
122
|
options.publishDir ?? (await this.#savePath(options)) ?? undefined;
|
|
122
123
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
124
|
+
// Both dated by default and both overridable, separately: the archive's
|
|
125
|
+
// name is what a map client shows, and the filename is what somebody finds
|
|
126
|
+
// on disk. Tying them together only means one of the two is wrong whenever
|
|
127
|
+
// they should differ.
|
|
128
|
+
const when = new Date();
|
|
129
|
+
const archiveName = bakedArchiveName(resolved, {
|
|
130
|
+
name: options.name,
|
|
131
|
+
when,
|
|
132
|
+
});
|
|
126
133
|
|
|
127
134
|
const job = {
|
|
128
135
|
stackId,
|
|
@@ -138,7 +145,7 @@ export class BakeManager {
|
|
|
138
145
|
finishedAt: null,
|
|
139
146
|
error: null,
|
|
140
147
|
infoHash: null,
|
|
141
|
-
name: bakedName(resolved, {
|
|
148
|
+
name: bakedName(resolved, { filename: options.filename, when }),
|
|
142
149
|
publishDir,
|
|
143
150
|
};
|
|
144
151
|
this.#jobs.set(stackId, job);
|
|
@@ -273,7 +280,10 @@ export class BakeManager {
|
|
|
273
280
|
pauseMs: this.#config.stacks?.bakePauseMs ?? 0,
|
|
274
281
|
metadata: {
|
|
275
282
|
name: job.archiveName,
|
|
276
|
-
|
|
283
|
+
// Only what was asked for. Falling back to the recipe's own
|
|
284
|
+
// description would fill in a field the dialog showed as empty, which
|
|
285
|
+
// is a worse surprise than having no description at all.
|
|
286
|
+
description: options.description,
|
|
277
287
|
attribution: resolved.stack.attribution,
|
|
278
288
|
encoding: resolved.stack.output?.encoding,
|
|
279
289
|
encodingFactors: resolved.stack.output,
|
package/src/bake.js
CHANGED
|
@@ -228,23 +228,45 @@ function stamp(when = new Date()) {
|
|
|
228
228
|
/**
|
|
229
229
|
* What to call the file a bake writes.
|
|
230
230
|
*
|
|
231
|
-
* Dated, because successive bakes of one stack are successive builds
|
|
232
|
-
* map and two files cannot share a path.
|
|
233
|
-
* see `bakedMetadata` -- so a rebuild keeps its identity the way every other
|
|
234
|
-
* rebuild here does.
|
|
231
|
+
* Dated by default, because successive bakes of one stack are successive builds
|
|
232
|
+
* of one map and two files cannot share a path.
|
|
235
233
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
234
|
+
* A caller may name it instead, and what they ask for is reduced to a single
|
|
235
|
+
* path segment before it is used. That is not politeness: this name is joined
|
|
236
|
+
* to a directory, and a filename is exactly the kind of field somebody puts a
|
|
237
|
+
* slash in.
|
|
239
238
|
* @param {object} resolved - The resolved stack.
|
|
240
|
-
* @param {object} [options] - `
|
|
241
|
-
* @returns {string} - A filename
|
|
239
|
+
* @param {object} [options] - `filename` to choose one outright, and `when`.
|
|
240
|
+
* @returns {string} - A filename, always ending `.pmtiles`.
|
|
242
241
|
*/
|
|
243
242
|
export function bakedName(resolved, options = {}) {
|
|
244
|
-
const
|
|
243
|
+
const requested = String(options.filename ?? '').trim();
|
|
244
|
+
if (requested) {
|
|
245
|
+
const stem = safeSegment(requested.replace(/\.pmtiles$/i, ''));
|
|
246
|
+
if (stem) return `${stem}.pmtiles`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const title = resolved.stack.title ?? resolved.stack.id;
|
|
245
250
|
const slug = safeSegment(title) || 'stack';
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
return `${slug}-${stamp(options.when)}.pmtiles`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* What to call the archive itself.
|
|
256
|
+
*
|
|
257
|
+
* Dated too, and separately from the file. Nothing in this project looks an
|
|
258
|
+
* archive up by name -- `/latest/<category>/` follows a category and takes the
|
|
259
|
+
* newest by date -- so a dated name costs nothing and says which build you are
|
|
260
|
+
* looking at, which is the question somebody holding two of them has.
|
|
261
|
+
* @param {object} resolved - The resolved stack.
|
|
262
|
+
* @param {object} [options] - `name` to choose one outright, and `when`.
|
|
263
|
+
* @returns {string} - The name.
|
|
264
|
+
*/
|
|
265
|
+
export function bakedArchiveName(resolved, options = {}) {
|
|
266
|
+
const explicit = String(options.name ?? '').trim();
|
|
267
|
+
if (explicit) return explicit;
|
|
268
|
+
const title = resolved.stack.title ?? resolved.stack.id;
|
|
269
|
+
return `${title} ${stamp(options.when)}`;
|
|
248
270
|
}
|
|
249
271
|
|
|
250
272
|
/**
|
package/src/web/index.html
CHANGED
|
@@ -661,19 +661,28 @@
|
|
|
661
661
|
|
|
662
662
|
<div class="field">
|
|
663
663
|
<label for="bake-name">Name</label>
|
|
664
|
-
<input id="bake-name" placeholder="Terrain" />
|
|
664
|
+
<input id="bake-name" placeholder="Terrain 20260101" />
|
|
665
665
|
<div class="sub">
|
|
666
|
-
What the archive
|
|
667
|
-
|
|
668
|
-
|
|
666
|
+
What the archive calls itself, and what a map client shows. Dated by
|
|
667
|
+
default, because two builds of one map are easier to tell apart that
|
|
668
|
+
way — nothing here looks an archive up by name, so it is free to say
|
|
669
|
+
whatever is useful.
|
|
669
670
|
</div>
|
|
670
671
|
</div>
|
|
671
672
|
|
|
673
|
+
<div class="field">
|
|
674
|
+
<label for="bake-file">Filename</label>
|
|
675
|
+
<input id="bake-file" placeholder="Terrain-20260101.pmtiles" />
|
|
676
|
+
<div class="sub" id="bake-filename"></div>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
672
679
|
<div class="field">
|
|
673
680
|
<label for="bake-description">Description</label>
|
|
674
|
-
<input id="bake-description" placeholder="
|
|
681
|
+
<input id="bake-description" placeholder="" />
|
|
675
682
|
<div class="sub">
|
|
676
|
-
Optional
|
|
683
|
+
Optional, and empty by default — what a stack merges is the
|
|
684
|
+
operator's business and not something to guess at. The date this was
|
|
685
|
+
baked is recorded here whether or not anything else is.
|
|
677
686
|
</div>
|
|
678
687
|
</div>
|
|
679
688
|
|
|
@@ -7742,10 +7751,17 @@ Every piece is hashed against the ` +
|
|
|
7742
7751
|
* @returns {Promise<void>} - Resolves once it is asked for or dismissed.
|
|
7743
7752
|
*/
|
|
7744
7753
|
async function openBakeDialog(id, stack) {
|
|
7754
|
+
const title = stack?.title ?? id;
|
|
7755
|
+
const day = new Date().toISOString().slice(0, 10).replaceAll('-', '');
|
|
7756
|
+
|
|
7745
7757
|
$('bake-stack').textContent = id;
|
|
7746
7758
|
$('bake-error').textContent = '';
|
|
7747
|
-
$('bake-name').value =
|
|
7748
|
-
$('bake-
|
|
7759
|
+
$('bake-name').value = `${title} ${day}`;
|
|
7760
|
+
$('bake-file').value = `${bakeSlug(title)}-${day}.pmtiles`;
|
|
7761
|
+
// Blank rather than the stack's own description. A recipe describes
|
|
7762
|
+
// how tiles are combined; an archive describes what it is, and only
|
|
7763
|
+
// the person exporting it knows that.
|
|
7764
|
+
$('bake-description').value = '';
|
|
7749
7765
|
$('bake-categories').value = (stack?.categories ?? []).join(', ');
|
|
7750
7766
|
showBakeFilename();
|
|
7751
7767
|
$('bake-location').innerHTML = locationPicker('bake-loc');
|
|
@@ -7754,28 +7770,46 @@ Every piece is hashed against the ` +
|
|
|
7754
7770
|
}
|
|
7755
7771
|
|
|
7756
7772
|
/**
|
|
7757
|
-
*
|
|
7773
|
+
* A name reduced to something usable as one path segment.
|
|
7774
|
+
*
|
|
7775
|
+
* The same rules `safeSegment` applies on the server, and a test holds
|
|
7776
|
+
* the two together — a field that shows one filename while the server
|
|
7777
|
+
* writes another is worse than a field that shows nothing.
|
|
7778
|
+
* @param {string} typed - What somebody entered.
|
|
7779
|
+
* @returns {string} - A safe segment, never empty.
|
|
7780
|
+
*/
|
|
7781
|
+
function bakeSlug(typed) {
|
|
7782
|
+
const slug = typed
|
|
7783
|
+
.replace(/[/\\<>:"|?*\u0000-\u001f\u007f -]/g, '-')
|
|
7784
|
+
.replace(/-{2,}/g, '-')
|
|
7785
|
+
.replace(/^[.\s-]+/, '')
|
|
7786
|
+
.replace(/[.\s-]+$/, '')
|
|
7787
|
+
.slice(0, 120)
|
|
7788
|
+
.replace(/[.\s-]+$/, '');
|
|
7789
|
+
return slug || 'stack';
|
|
7790
|
+
}
|
|
7791
|
+
|
|
7792
|
+
/**
|
|
7793
|
+
* Says what the file will actually be called, where that differs.
|
|
7758
7794
|
*
|
|
7759
|
-
*
|
|
7760
|
-
*
|
|
7761
|
-
*
|
|
7762
|
-
* before pressing the button beats finding it on disk afterwards.
|
|
7795
|
+
* A filename is exactly the kind of field somebody puts a slash in, and
|
|
7796
|
+
* the server reduces it to one path segment before joining it to a
|
|
7797
|
+
* directory. Saying so here means it is not a surprise.
|
|
7763
7798
|
* @returns {void}
|
|
7764
7799
|
*/
|
|
7765
7800
|
function showBakeFilename() {
|
|
7766
|
-
const typed = $('bake-
|
|
7767
|
-
const
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
$('bake-filename').textContent = `${slug}-${day}.pmtiles`;
|
|
7801
|
+
const typed = $('bake-file').value.trim();
|
|
7802
|
+
const note = $('bake-filename');
|
|
7803
|
+
if (!typed) {
|
|
7804
|
+
note.textContent = 'Left empty, it is named after the stack and dated.';
|
|
7805
|
+
return;
|
|
7806
|
+
}
|
|
7807
|
+
const settled = `${bakeSlug(typed.replace(/\.pmtiles$/i, ''))}.pmtiles`;
|
|
7808
|
+
note.textContent =
|
|
7809
|
+
settled === typed ? '' : `Saved as ${settled}`;
|
|
7776
7810
|
}
|
|
7777
7811
|
|
|
7778
|
-
$('bake-
|
|
7812
|
+
$('bake-file').addEventListener('input', showBakeFilename);
|
|
7779
7813
|
|
|
7780
7814
|
$('bake-form').addEventListener('submit', async (event) => {
|
|
7781
7815
|
if (event.submitter?.value === 'cancel') return;
|
|
@@ -7787,6 +7821,7 @@ Every piece is hashed against the ` +
|
|
|
7787
7821
|
.map((one) => one.trim())
|
|
7788
7822
|
.filter(Boolean);
|
|
7789
7823
|
const name = $('bake-name').value.trim();
|
|
7824
|
+
const filename = $('bake-file').value.trim();
|
|
7790
7825
|
const description = $('bake-description').value.trim();
|
|
7791
7826
|
|
|
7792
7827
|
try {
|
|
@@ -7796,6 +7831,7 @@ Every piece is hashed against the ` +
|
|
|
7796
7831
|
...chosenLocation('bake-loc'),
|
|
7797
7832
|
...(categories.length > 0 ? { categories } : {}),
|
|
7798
7833
|
...(name ? { name } : {}),
|
|
7834
|
+
...(filename ? { filename } : {}),
|
|
7799
7835
|
...(description ? { description } : {}),
|
|
7800
7836
|
},
|
|
7801
7837
|
});
|