pmtiles-swarm 0.21.0 → 0.22.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 +8 -0
- package/docs/serving-tiles.md +21 -0
- package/package.json +1 -1
- package/src/api.js +89 -48
- package/src/web/public.html +89 -22
package/CHANGELOG.md
CHANGED
|
@@ -58,6 +58,14 @@
|
|
|
58
58
|
`/api/categories`, filtered by the same `feedCategories` rule, so it can show nothing that was
|
|
59
59
|
not already published — and it is not the console, which stays on the admin port.
|
|
60
60
|
`publicIndex: false` turns it off, withdrawing the three paths it needs with it.
|
|
61
|
+
- **`GET /latest/` lists the categories, without a credential.** Everything else under
|
|
62
|
+
`/latest/` is public — the TileJSON, the torrent, the magnet, the per-category feed — so the
|
|
63
|
+
index of what it offers belongs beside them rather than behind the console's door. The public
|
|
64
|
+
front page now leads with categories rather than only listing archives, which is the more
|
|
65
|
+
useful handle for a visitor: an archive URL names one build and goes stale on the next, while a
|
|
66
|
+
category names whichever is current. Same builder as `/api/categories`, so the two cannot drift
|
|
67
|
+
apart. The page's footer and its noscript block no longer link `/api/` paths that answer 401 to
|
|
68
|
+
exactly the audience they are shown to.
|
|
61
69
|
- **The feed carries the BEP 46 identity.** `<pmtiles:mutable>` holds the magnet naming an
|
|
62
70
|
archive's publishing key, so a consumer following a category across rebuilds can read the
|
|
63
71
|
public key straight out of the feed instead of fetching a TileJSON to find it — the key rides
|
package/docs/serving-tiles.md
CHANGED
|
@@ -41,6 +41,27 @@ swarm on every attempt, doing work that could never succeed.
|
|
|
41
41
|
|
|
42
42
|
See [internals.md](internals.md#serving-an-mbtiles-archive).
|
|
43
43
|
|
|
44
|
+
## The category index
|
|
45
|
+
|
|
46
|
+
`GET /latest/` lists every category this node publishes, with the endpoints
|
|
47
|
+
that resolve to each one's newest build — the TileJSON, the `.torrent`, the
|
|
48
|
+
magnet, the per-category feed, and the style URL with the magnet in its
|
|
49
|
+
fragment.
|
|
50
|
+
|
|
51
|
+
Public, and deliberately so. Everything else under `/latest/` is — the
|
|
52
|
+
TileJSON, the torrent, the magnet, the feed — so the index of what `/latest/`
|
|
53
|
+
offers belongs beside them rather than behind the console's door.
|
|
54
|
+
`/api/categories` returns the identical list from the identical builder, and is
|
|
55
|
+
guarded only because everything under `/api/` is.
|
|
56
|
+
|
|
57
|
+
A category is also the handle worth publishing. An archive URL names one build
|
|
58
|
+
and goes stale on the next; a category names whichever is current, so a style
|
|
59
|
+
pointing at one keeps working across rebuilds.
|
|
60
|
+
|
|
61
|
+
Filtered by `feedCategories` and by whatever token was presented, the same as
|
|
62
|
+
the catalogue and the feeds — so it can show nothing this node was not already
|
|
63
|
+
publishing.
|
|
64
|
+
|
|
44
65
|
## Missing tiles
|
|
45
66
|
|
|
46
67
|
A tile the archive does not hold answers **404 for raster and 204 for vector**,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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
|
@@ -1744,9 +1744,19 @@ export function createApp({
|
|
|
1744
1744
|
// what makes a tile immutable, but leaves nothing for a style to point at
|
|
1745
1745
|
// that survives a rebuild — and until now the endpoints that give you one
|
|
1746
1746
|
// were documented rather than discoverable.
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1747
|
+
/**
|
|
1748
|
+
* Every category, with the endpoints that resolve to its newest build.
|
|
1749
|
+
*
|
|
1750
|
+
* Shared by the guarded `/api/categories` and the public `/latest/`, which
|
|
1751
|
+
* want exactly the same answer — one is the console asking and the other is
|
|
1752
|
+
* a stranger, and neither is told anything the other is not. Filtering runs
|
|
1753
|
+
* through `publishesEntry`, so `feedCategories` and whatever token was
|
|
1754
|
+
* presented decide what appears, the same as everywhere else.
|
|
1755
|
+
* @param {import('express').Request} req - The request, for its credential and base URL.
|
|
1756
|
+
* @returns {object[]} - One row per category, sorted by name.
|
|
1757
|
+
*/
|
|
1758
|
+
const describeCategories = (req) => {
|
|
1759
|
+
{
|
|
1750
1760
|
const base = baseUrl(req);
|
|
1751
1761
|
const visible = catalog
|
|
1752
1762
|
.list()
|
|
@@ -1759,51 +1769,82 @@ export function createApp({
|
|
|
1759
1769
|
}
|
|
1760
1770
|
}
|
|
1761
1771
|
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
servable,
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1772
|
+
return [...counts.keys()].sort().map((category) => {
|
|
1773
|
+
const newest = newestIn(category, req);
|
|
1774
|
+
// Only PMTiles has tiles to serve, so a category whose newest build
|
|
1775
|
+
// is an MBTiles archive gets a feed and a torrent but no tile
|
|
1776
|
+
// endpoint — the same rule as an individual archive.
|
|
1777
|
+
const servable =
|
|
1778
|
+
Boolean(newest?.pmtiles) && (newest.kind ?? 'pmtiles') === 'pmtiles';
|
|
1779
|
+
|
|
1780
|
+
return {
|
|
1781
|
+
category,
|
|
1782
|
+
archives: counts.get(category),
|
|
1783
|
+
newest: newest && {
|
|
1784
|
+
infoHash: newest.infoHash,
|
|
1785
|
+
name: newest.name,
|
|
1786
|
+
size: newest.size,
|
|
1787
|
+
createdAt: newest.createdAt,
|
|
1788
|
+
kind: newest.kind ?? 'pmtiles',
|
|
1789
|
+
},
|
|
1790
|
+
servable,
|
|
1791
|
+
endpoints: {
|
|
1792
|
+
tileJson: servable ? `${base}/latest/${category}/tiles.json` : null,
|
|
1793
|
+
// The same URL with a magnet in the fragment, which is what a
|
|
1794
|
+
// style should carry. A fragment is never sent in a request, so
|
|
1795
|
+
// an ordinary client fetches the TileJSON and ignores it, while
|
|
1796
|
+
// a swarm-aware one has the magnet before it makes any call --
|
|
1797
|
+
// and can therefore still start when this server cannot answer.
|
|
1798
|
+
//
|
|
1799
|
+
// The mutable magnet where there is one, because a category is
|
|
1800
|
+
// precisely where an infohash goes stale: it names the category
|
|
1801
|
+
// and resolves the current build over the DHT. Otherwise the
|
|
1802
|
+
// newest build's own magnet, which pins that build but still
|
|
1803
|
+
// beats a blank map when the fallback is needed at all.
|
|
1804
|
+
styleUrl: servable ? styleUrlFor(category, newest, base) : null,
|
|
1805
|
+
torrent: `${base}/latest/${category}/archive.torrent`,
|
|
1806
|
+
magnet: `${base}/latest/${category}/magnet`,
|
|
1807
|
+
feed: `${base}/feed/${category}.xml`,
|
|
1808
|
+
latestFeed: `${base}/latest/${category}.xml`,
|
|
1809
|
+
},
|
|
1810
|
+
};
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
// The console's view. Identical to the public one below, and guarded only
|
|
1816
|
+
// because everything under /api/ is.
|
|
1817
|
+
app.get(
|
|
1818
|
+
'/api/categories',
|
|
1819
|
+
route(async (req, res) => {
|
|
1820
|
+
res.json(describeCategories(req));
|
|
1821
|
+
}),
|
|
1822
|
+
);
|
|
1823
|
+
|
|
1824
|
+
// The same list, without a credential.
|
|
1825
|
+
//
|
|
1826
|
+
// Everything under /latest/ is public already — the TileJSON, the torrent,
|
|
1827
|
+
// the magnet, the per-category feed — so the index of what /latest/ offers
|
|
1828
|
+
// belongs here rather than behind the console's door. A category is also the
|
|
1829
|
+
// handle a person actually wants: an infohash names one build, and this
|
|
1830
|
+
// names whichever is current, so a style pointing at it keeps working across
|
|
1831
|
+
// rebuilds.
|
|
1832
|
+
//
|
|
1833
|
+
// Not a second implementation. It is the same builder, filtered by the same
|
|
1834
|
+
// rule, so the two cannot drift into disagreeing about what is published.
|
|
1835
|
+
app.get(
|
|
1836
|
+
'/latest/',
|
|
1837
|
+
route(async (req, res) => {
|
|
1838
|
+
res.setHeader('access-control-allow-origin', '*');
|
|
1839
|
+
// Short, because the answer changes when a build lands.
|
|
1840
|
+
res.setHeader('cache-control', 'public, max-age=60');
|
|
1841
|
+
res.json({
|
|
1842
|
+
// Named so a consumer can refuse a document it does not understand,
|
|
1843
|
+
// the same as the catalogue does.
|
|
1844
|
+
format: 'pmtiles-swarm-categories/1',
|
|
1845
|
+
generatedAt: new Date().toISOString(),
|
|
1846
|
+
categories: describeCategories(req),
|
|
1847
|
+
});
|
|
1807
1848
|
}),
|
|
1808
1849
|
);
|
|
1809
1850
|
|
package/src/web/public.html
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
The public front door, served at / on the public listener when the console
|
|
4
4
|
has an admin port of its own.
|
|
5
5
|
|
|
6
|
-
Reads /
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
Reads /latest/ for the categories and, for the archives, /api/catalog where a
|
|
7
|
+
credential allows it and /feed.xml where one does not — everything under /api/
|
|
8
|
+
is guarded on a node with any authentication configured, which is most of them,
|
|
9
|
+
and a front page that answers 401 to its own audience is worse than none.
|
|
10
|
+
|
|
11
|
+
All three are filtered by feedCategories, so this page shows exactly what the
|
|
12
|
+
node publishes and cannot show anything else. It is deliberately plain: no
|
|
13
|
+
build step, no dependencies, and it degrades to a readable list of links if the
|
|
10
14
|
script does not run.
|
|
11
15
|
-->
|
|
12
16
|
<html lang="en">
|
|
@@ -167,7 +171,7 @@
|
|
|
167
171
|
<noscript>
|
|
168
172
|
<p class="failed">
|
|
169
173
|
This page lists the archives using JavaScript. Without it, the same
|
|
170
|
-
information is at <a href="/
|
|
174
|
+
information is at <a href="/latest/">/latest/</a> as JSON and
|
|
171
175
|
<a href="/feed.xml">/feed.xml</a> as RSS.
|
|
172
176
|
</p>
|
|
173
177
|
</noscript>
|
|
@@ -176,8 +180,7 @@
|
|
|
176
180
|
<div id="archives"><p class="empty">Loading…</p></div>
|
|
177
181
|
|
|
178
182
|
<footer>
|
|
179
|
-
<a href="/
|
|
180
|
-
<a href="/api/categories">categories JSON</a> ·
|
|
183
|
+
<a href="/latest/">categories JSON</a> ·
|
|
181
184
|
<a href="/feed.xml">RSS feed</a> ·
|
|
182
185
|
<a href="/health">health</a>
|
|
183
186
|
</footer>
|
|
@@ -280,27 +283,85 @@
|
|
|
280
283
|
|
|
281
284
|
// Categories are the handle that survives a rebuild, so they go first —
|
|
282
285
|
// a style should point at one of these rather than at an infohash.
|
|
286
|
+
// Categories first, and given the same weight as an archive, because
|
|
287
|
+
// for most visitors they are the more useful thing: an archive URL names
|
|
288
|
+
// one build and goes stale on the next, while a category names whichever
|
|
289
|
+
// is current and keeps working across rebuilds. A style should point at
|
|
290
|
+
// one of these.
|
|
283
291
|
const renderCategories = (categories) => {
|
|
284
292
|
const host = document.getElementById('categories');
|
|
285
|
-
|
|
293
|
+
host.textContent = '';
|
|
294
|
+
const usable = categories.filter((c) => c.endpoints);
|
|
286
295
|
if (usable.length === 0) return;
|
|
287
296
|
|
|
288
297
|
host.append(el('h2', null, 'Categories'));
|
|
289
|
-
|
|
298
|
+
host.append(
|
|
299
|
+
el(
|
|
300
|
+
'p',
|
|
301
|
+
'facts',
|
|
302
|
+
'These follow the newest build. Point a style at one of these ' +
|
|
303
|
+
'rather than at an archive below, and it keeps working when the ' +
|
|
304
|
+
'archive is rebuilt.',
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
|
|
290
308
|
for (const entry of usable) {
|
|
291
|
-
// `category` and `archives` are what
|
|
309
|
+
// `category` and `archives` are what the endpoint actually calls
|
|
292
310
|
// these; `name` and `count` would read better and are not there.
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
311
|
+
const card = el('div', 'archive');
|
|
312
|
+
const head = el('header');
|
|
313
|
+
head.append(el('span', 'name', entry.category));
|
|
314
|
+
if (Number.isFinite(entry.archives)) {
|
|
315
|
+
head.append(
|
|
316
|
+
el(
|
|
317
|
+
'span',
|
|
318
|
+
'tag',
|
|
319
|
+
`${entry.archives} build${entry.archives === 1 ? '' : 's'}`,
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
card.append(head);
|
|
324
|
+
|
|
325
|
+
if (entry.newest?.name) {
|
|
326
|
+
const facts = [`newest: ${entry.newest.name}`];
|
|
327
|
+
const size = bytes(entry.newest.size);
|
|
328
|
+
if (size) facts.push(size);
|
|
329
|
+
card.append(el('p', 'facts', facts.join(' · ')));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const links = el('div', 'links');
|
|
333
|
+
const add = (href, text) => {
|
|
334
|
+
const a = el('a', null, text);
|
|
335
|
+
a.href = href;
|
|
336
|
+
links.append(a);
|
|
337
|
+
};
|
|
338
|
+
const ends = entry.endpoints;
|
|
339
|
+
if (ends.tileJson) add(ends.tileJson, 'TileJSON');
|
|
340
|
+
if (ends.torrent) add(ends.torrent, '.torrent');
|
|
341
|
+
if (ends.magnet) add(ends.magnet, 'magnet');
|
|
342
|
+
if (ends.feed) add(ends.feed, 'RSS');
|
|
343
|
+
card.append(links);
|
|
344
|
+
|
|
345
|
+
const urls = el('div', 'urls');
|
|
346
|
+
// The one worth copying: the TileJSON URL with the magnet in its
|
|
347
|
+
// fragment, which a plain client fetches over HTTP and a swarm-aware
|
|
348
|
+
// one joins directly.
|
|
349
|
+
if (ends.styleUrl) {
|
|
350
|
+
urls.append(
|
|
351
|
+
urlRow('style URL', new URL(ends.styleUrl, location.href).href),
|
|
352
|
+
);
|
|
353
|
+
} else if (ends.tileJson) {
|
|
354
|
+
urls.append(
|
|
355
|
+
urlRow('TileJSON', new URL(ends.tileJson, location.href).href),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (urls.childElementCount > 0) card.append(urls);
|
|
359
|
+
|
|
360
|
+
host.append(card);
|
|
299
361
|
}
|
|
300
|
-
|
|
362
|
+
|
|
301
363
|
host.append(el('h2', null, 'Archives'));
|
|
302
364
|
};
|
|
303
|
-
|
|
304
365
|
const PMTILES_NS =
|
|
305
366
|
'https://github.com/TechIdiots-LLC/pmtiles-swarm/ns/1.0';
|
|
306
367
|
|
|
@@ -381,11 +442,17 @@
|
|
|
381
442
|
|
|
382
443
|
render(archives.filter((archive) => archive.infoHash));
|
|
383
444
|
|
|
384
|
-
//
|
|
385
|
-
//
|
|
445
|
+
// /latest/ rather than /api/categories: the same list, without a
|
|
446
|
+
// credential. This is the part most visitors actually want -- an
|
|
447
|
+
// archive URL names one build, a category names whichever is current
|
|
448
|
+
// -- so it is worth more than a best-effort attempt at a guarded
|
|
449
|
+
// endpoint that answers 401 to exactly the audience this page is for.
|
|
386
450
|
try {
|
|
387
|
-
const response = await fetch('/
|
|
388
|
-
if (response.ok)
|
|
451
|
+
const response = await fetch('/latest/');
|
|
452
|
+
if (response.ok) {
|
|
453
|
+
const body = await response.json();
|
|
454
|
+
renderCategories(body.categories ?? []);
|
|
455
|
+
}
|
|
389
456
|
} catch {
|
|
390
457
|
// Nothing to say. The archives above are the substance.
|
|
391
458
|
}
|