pmtiles-swarm 0.47.1 → 0.48.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 +19 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/api.js +62 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.48.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **An archive can now be read as a file, by byte range.** `GET /archives/<infohash>/archive.pmtiles`
|
|
13
|
+
answers `Accept-Ranges: bytes`, a `206` with `Content-Range` for a range and `416` for one it cannot
|
|
14
|
+
satisfy. Every PMTiles consumer there is — pmtiles.js, tileserver-gl, go-pmtiles, QGIS — reads one
|
|
15
|
+
file over HTTP that way, and until now this node offered tiles, which is a different protocol: using
|
|
16
|
+
it as an origin meant a copy of the file somewhere else. This is the file, at an address that does
|
|
17
|
+
not depend on knowing where the node keeps it.
|
|
18
|
+
|
|
19
|
+
Complete archives only. A partial file answers a range with whatever is at that offset, which for a
|
|
20
|
+
torrent's sparse allocation is zeroes — worse than a refusal, because it looks like data. An
|
|
21
|
+
incomplete archive answers `409` and says so.
|
|
22
|
+
|
|
23
|
+
It is also, by construction, a valid BEP 19 web seed: that specification is "an HTTP URL that serves
|
|
24
|
+
the file and honours Range". Publishing it as one is a separate decision and a later change — a node
|
|
25
|
+
is not obliged to offer 700 GiB to strangers because it can.
|
|
26
|
+
|
|
27
|
+
### 🐞 Bug fixes
|
|
28
|
+
|
|
10
29
|
## 0.47.1
|
|
11
30
|
### ✨ Features and improvements
|
|
12
31
|
|
package/README.md
CHANGED
|
@@ -783,6 +783,7 @@ which the endpoint answers 501.
|
|
|
783
783
|
| `GET` | `/health` | 200 when this node can serve, 503 when its engine cannot — **public**, no credential, for a load balancer |
|
|
784
784
|
| `GET` | `/archives/:infoHash/ready` | Whether this node can serve _this_ archive yet: 200 ready, 503 not yet, 415 never — **public** |
|
|
785
785
|
| `GET` | `/archives/:infoHash/archive.torrent` | The `.torrent` a torrent-aware client joins with — **public** |
|
|
786
|
+
| `GET` | `/archives/:infoHash/archive.pmtiles` | The archive itself, by byte range — what any PMTiles reader wants, and a valid web seed. Complete archives only — **public** |
|
|
786
787
|
| `GET` | `/archives/:infoHash/preview` | Map preview for one archive — admin, not public |
|
|
787
788
|
| `GET` | `/latest/:category/tiles.json`, `/:name.torrent`, `/magnet` | The newest build in a category — **public**. The torrent name is yours to choose, so a link can read `planetiler-openmaptiles-latest.torrent`; it redirects to the immutable URL, which names the download after the build it actually is |
|
|
788
789
|
| `GET` | `/feed.xml`, `/feed/:category.xml`, `/latest/:category.xml` | RSS — **public** |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.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
|
@@ -2429,6 +2429,68 @@ export function createApp({
|
|
|
2429
2429
|
}),
|
|
2430
2430
|
);
|
|
2431
2431
|
|
|
2432
|
+
/**
|
|
2433
|
+
* The archive itself, by byte range.
|
|
2434
|
+
*
|
|
2435
|
+
* Every PMTiles consumer there is — pmtiles.js, tileserver-gl, go-pmtiles,
|
|
2436
|
+
* QGIS — reads one file over HTTP with Range requests. Until now this node
|
|
2437
|
+
* offered tiles, which is a different protocol, so using it as an origin
|
|
2438
|
+
* meant either its own tile endpoint or a copy of the file somewhere else.
|
|
2439
|
+
* This is the file, at an address that does not depend on knowing where the
|
|
2440
|
+
* node keeps it.
|
|
2441
|
+
*
|
|
2442
|
+
* It is also, by construction, a valid BEP 19 web seed: that specification
|
|
2443
|
+
* is "an HTTP URL that serves the file and honours Range", which is exactly
|
|
2444
|
+
* this. Publishing it is a separate decision and a later one — a node is not
|
|
2445
|
+
* obliged to offer 700 GiB to strangers because it can.
|
|
2446
|
+
*
|
|
2447
|
+
* Complete archives only. A partial file would answer a range with whatever
|
|
2448
|
+
* happened to be at that offset, which for a torrent's sparse allocation is
|
|
2449
|
+
* zeroes: worse than a refusal, because it looks like data.
|
|
2450
|
+
*/
|
|
2451
|
+
app.get('/archives/:infoHash/archive.pmtiles', (req, res) => {
|
|
2452
|
+
const entry = catalog.get(req.params.infoHash);
|
|
2453
|
+
if (!entry) return res.status(404).json({ error: 'not found' });
|
|
2454
|
+
|
|
2455
|
+
if (entry.complete === false) {
|
|
2456
|
+
return res.status(409).json({
|
|
2457
|
+
error:
|
|
2458
|
+
'this archive is not complete here, so a byte range would answer ' +
|
|
2459
|
+
'with unwritten space rather than data',
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
const file = entry.savePath ? path.join(entry.savePath, entry.name) : null;
|
|
2463
|
+
if (!file) return res.status(404).json({ error: 'no file for it here' });
|
|
2464
|
+
|
|
2465
|
+
// sendFile does the range work: 206 with Content-Range, 416 for one that
|
|
2466
|
+
// cannot be satisfied, HEAD, and the conditional headers a cache needs.
|
|
2467
|
+
// Reimplementing that is how a subtly wrong Content-Range gets shipped.
|
|
2468
|
+
res.sendFile(
|
|
2469
|
+
file,
|
|
2470
|
+
{
|
|
2471
|
+
acceptRanges: true,
|
|
2472
|
+
// A tile archive is immutable — it is addressed by the hash of its
|
|
2473
|
+
// own contents, so a byte at an offset is the same byte for ever.
|
|
2474
|
+
maxAge: '1y',
|
|
2475
|
+
immutable: true,
|
|
2476
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
2477
|
+
},
|
|
2478
|
+
(error) => {
|
|
2479
|
+
if (!error || res.headersSent) return;
|
|
2480
|
+
const missing = error.code === 'ENOENT';
|
|
2481
|
+
// sendFile reports a range it cannot satisfy as a 416 on the error,
|
|
2482
|
+
// which is an answer rather than a fault — passing it through as 500
|
|
2483
|
+
// would tell a client its request was our problem.
|
|
2484
|
+
const status = missing ? 404 : (error.status ?? 500);
|
|
2485
|
+
res.status(status).json({
|
|
2486
|
+
error: missing
|
|
2487
|
+
? 'the catalog has this archive but its file is not there'
|
|
2488
|
+
: error.message,
|
|
2489
|
+
});
|
|
2490
|
+
},
|
|
2491
|
+
);
|
|
2492
|
+
});
|
|
2493
|
+
|
|
2432
2494
|
app.get(
|
|
2433
2495
|
'/archives/:infoHash/archive.torrent',
|
|
2434
2496
|
route(async (req, res) => {
|