pmtiles-swarm 0.2.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 +53 -0
- package/LICENSE +28 -0
- package/NOTICE.md +47 -0
- package/README.md +172 -0
- package/docs/architecture-diagram.md +287 -0
- package/docs/engines.md +140 -0
- package/docs/publishing.md +387 -0
- package/docs/serving-tiles.md +218 -0
- package/docs/subscribing.md +187 -0
- package/package.json +56 -0
- package/src/api.js +472 -0
- package/src/catalog.js +159 -0
- package/src/config.js +231 -0
- package/src/engines/libtorrent.js +384 -0
- package/src/engines/qbittorrent.js +320 -0
- package/src/engines/types.js +59 -0
- package/src/engines/webtorrent.js +264 -0
- package/src/feed.js +226 -0
- package/src/file-source.js +67 -0
- package/src/index.js +177 -0
- package/src/library.js +567 -0
- package/src/mutable.js +197 -0
- package/src/origin.js +205 -0
- package/src/pmtiles-probe.js +94 -0
- package/src/read-engine.js +175 -0
- package/src/sources.js +245 -0
- package/src/subscriptions.js +158 -0
- package/src/tilejson.js +112 -0
- package/src/tiles.js +284 -0
- package/src/torrent-create.js +250 -0
- package/src/warm.js +251 -0
- package/src/watch.js +98 -0
- package/src/web/index.html +254 -0
package/docs/engines.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Seeding engines
|
|
2
|
+
|
|
3
|
+
pmtiles-swarm does not implement BitTorrent. Seeding is delegated to an engine behind a
|
|
4
|
+
small interface ([`src/engines/types.js`](../src/engines/types.js)), so the part that is
|
|
5
|
+
genuinely ours — the catalog, torrent creation, feeds, watch folders — stays independent
|
|
6
|
+
of it.
|
|
7
|
+
|
|
8
|
+
Three exist. Pick by what the node is for.
|
|
9
|
+
|
|
10
|
+
| | libtorrent | qBittorrent | WebTorrent |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| Runs as | sidecar process | your existing instance | in-process |
|
|
13
|
+
| Extra install | `python3-libtorrent` | qBittorrent | none |
|
|
14
|
+
| BitTorrent v2 | yes | yes | **no** |
|
|
15
|
+
| Creates hybrid v1+v2 | yes | via its own UI | no |
|
|
16
|
+
| Piece-level control | yes | **no** | yes |
|
|
17
|
+
| Proper cache mode | yes | approximated | yes |
|
|
18
|
+
| Serves browser peers | no | no | **yes** |
|
|
19
|
+
| Bulk seeding at TB scale | yes | yes | weaker |
|
|
20
|
+
|
|
21
|
+
## libtorrent (recommended)
|
|
22
|
+
|
|
23
|
+
The most capable: BitTorrent v2 and hybrid torrents, resume data so a restart does not
|
|
24
|
+
re-hash the store, piece-level control, and seeding that holds at multi-terabyte scale.
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"engine": "libtorrent",
|
|
29
|
+
"libtorrent": {
|
|
30
|
+
"savePath": "./data/torrents-data",
|
|
31
|
+
"resumeDir": "./data/resume",
|
|
32
|
+
"listen": "0.0.0.0:6881",
|
|
33
|
+
"python": "python3"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Install libtorrent's Python bindings:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
apt install python3-libtorrent # Debian/Ubuntu
|
|
42
|
+
brew install libtorrent-rasterbar # macOS
|
|
43
|
+
pip install libtorrent # anywhere with wheels, incl. Windows
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Why a sidecar and not a native binding
|
|
47
|
+
|
|
48
|
+
Node has no maintained libtorrent binding. The npm packages (`libtorrent`,
|
|
49
|
+
`node-libtorrent`, `libtorrent-rasterbar`) are abandoned 2022 stubs, and the one live
|
|
50
|
+
fork ships no prebuilt binaries and exposes none of `set_piece_deadline`, `read_piece`,
|
|
51
|
+
v2, or resume data — which are the four reasons to want libtorrent at all.
|
|
52
|
+
|
|
53
|
+
A binding would also need libtorrent headers, Boost and a compiler on every machine,
|
|
54
|
+
where the sidecar needs one distro package. The protocol is line-delimited JSON over
|
|
55
|
+
stdin/stdout, so if a proper N-API addon with prebuilt binaries appears later, only the
|
|
56
|
+
far side of the pipe changes — [`src/engines/libtorrent.js`](../src/engines/libtorrent.js)
|
|
57
|
+
stays as it is.
|
|
58
|
+
|
|
59
|
+
## qBittorrent
|
|
60
|
+
|
|
61
|
+
Drives an instance you already run, over its WebUI API. Choose it when you have an
|
|
62
|
+
existing library you do not want to disturb: `POST /api/adopt` imports everything it
|
|
63
|
+
already seeds without re-hashing a byte.
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"engine": "qbittorrent",
|
|
68
|
+
"qbittorrent": {
|
|
69
|
+
"url": "http://127.0.0.1:8080",
|
|
70
|
+
"username": "admin",
|
|
71
|
+
"password": "…"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Omit `username` if the WebUI bypasses authentication for your subnet.
|
|
77
|
+
|
|
78
|
+
**Its limitation is cache mode.** That needs piece-level selection, and the WebUI exposes
|
|
79
|
+
only per-file priorities — which for a single-file archive is all or nothing. Cache mode
|
|
80
|
+
therefore adds the torrent stopped and leaves on-demand reads to a client that can do
|
|
81
|
+
them, such as a tileserver-gl instance using `pmtiles-torrent`.
|
|
82
|
+
|
|
83
|
+
No qBittorrent code is used here; this is an HTTP client for its documented API. See
|
|
84
|
+
[NOTICE.md](../NOTICE.md) for why that distinction matters.
|
|
85
|
+
|
|
86
|
+
## WebTorrent
|
|
87
|
+
|
|
88
|
+
Self-contained, no external dependency, and the **only** engine that can serve browser
|
|
89
|
+
peers — browsers speak WebRTC, conventional clients speak TCP/uTP, and they cannot see
|
|
90
|
+
each other directly. A WebTorrent node is what bridges the two halves of a swarm.
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"engine": "webtorrent",
|
|
95
|
+
"webtorrent": { "savePath": "./data/torrents-data" }
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
It is BitTorrent v1 only (no BEP 52) and a weaker bulk seeder than libtorrent. For a
|
|
100
|
+
multi-terabyte library, prefer libtorrent and run WebTorrent alongside as the browser
|
|
101
|
+
bridge.
|
|
102
|
+
|
|
103
|
+
### A required dependency pin
|
|
104
|
+
|
|
105
|
+
`package.json` pins `uint8-util` to `2.2.5` through `overrides`. Version 2.3.0 rewrote
|
|
106
|
+
`arr2hex` as `Buffer.from(data.buffer, …)`, which throws on the hex-string infohash that
|
|
107
|
+
webtorrent's `Torrent._onTorrentId` passes it — breaking **every magnet add**. webtorrent
|
|
108
|
+
declares `^2.2.5`, so a minor bump silently breaks it. Remove the pin once webtorrent
|
|
109
|
+
fixes the call site.
|
|
110
|
+
|
|
111
|
+
## Cache mode across engines
|
|
112
|
+
|
|
113
|
+
Joining a torrent defaults to `cache`, not `mirror`: committing a disk to a copy of
|
|
114
|
+
something that may be hundreds of gigabytes should be a decision, not a side effect.
|
|
115
|
+
|
|
116
|
+
| Mode | Disk | Purpose |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `mirror` | the whole archive | Full seeder, adds redundancy |
|
|
119
|
+
| `cache` | only what is read | Serving tiles from a huge archive on a small disk |
|
|
120
|
+
|
|
121
|
+
Implementing cache mode correctly is subtler than it looks. Two bugs found while testing
|
|
122
|
+
the libtorrent engine, both of which would have silently broken it:
|
|
123
|
+
|
|
124
|
+
- It must set **file priorities to 0**, not libtorrent's `upload_mode` flag. Upload mode
|
|
125
|
+
refuses to download anything at all, which defeats the purpose — `read_piece` raises an
|
|
126
|
+
individual piece back to priority 7 to fetch it on demand, and priority 0 permits that
|
|
127
|
+
where upload mode does not.
|
|
128
|
+
- A torrent with nothing wanted looks idle to libtorrent's auto-manager, which **pauses
|
|
129
|
+
it** — and a paused torrent stops seeding, so the node would have joined the swarm and
|
|
130
|
+
contributed nothing. Cache-mode torrents are added with auto-management off.
|
|
131
|
+
|
|
132
|
+
A correctly configured cache-mode node reports state `cache`, holds zero bytes until
|
|
133
|
+
something reads from it, and still serves whatever pieces it has picked up.
|
|
134
|
+
|
|
135
|
+
## Writing another engine
|
|
136
|
+
|
|
137
|
+
Implement `connect`, `add`, `remove`, `list`, `get`, `destroy`, and optionally `peers`.
|
|
138
|
+
The interface is deliberately small; see
|
|
139
|
+
[`src/engines/types.js`](../src/engines/types.js) for the contract and
|
|
140
|
+
[`src/engines/webtorrent.js`](../src/engines/webtorrent.js) for the shortest example.
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Publishing archives
|
|
2
|
+
|
|
3
|
+
How an archive gets into the swarm, and the decisions that matter when it does.
|
|
4
|
+
|
|
5
|
+
## The four ways in
|
|
6
|
+
|
|
7
|
+
| Input | What happens | Hashes? |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Local `.pmtiles` path | A torrent is created; the data stays where it is | Yes |
|
|
10
|
+
| Remote `.pmtiles` URL | Streamed past the hasher; origin becomes a web seed | Yes |
|
|
11
|
+
| Magnet or `.torrent` | Joined — nothing is created | No |
|
|
12
|
+
| `POST /api/adopt` | Everything the engine already seeds is imported | No |
|
|
13
|
+
|
|
14
|
+
Publishers create; everyone else joins. `adopt` is the migration path for an existing
|
|
15
|
+
library — it re-hashes nothing, so bringing across 50 torrents is instant.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Create from a local archive
|
|
19
|
+
curl -X POST localhost:8090/api/torrents -H 'content-type: application/json' \
|
|
20
|
+
-d '{"path": "/mnt/maps/planet.pmtiles", "category": "basemaps"}'
|
|
21
|
+
|
|
22
|
+
# Join an existing torrent
|
|
23
|
+
curl -X POST localhost:8090/api/torrents -H 'content-type: application/json' \
|
|
24
|
+
-d '{"magnet": "magnet:?xt=urn:btih:5e1c..."}'
|
|
25
|
+
|
|
26
|
+
# Upload a .torrent directly
|
|
27
|
+
curl -X POST localhost:8090/api/torrents \
|
|
28
|
+
-H 'content-type: application/x-bittorrent' --data-binary @planet.pmtiles.torrent
|
|
29
|
+
|
|
30
|
+
# Import what your torrent client already holds
|
|
31
|
+
curl -X POST localhost:8090/api/adopt
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Web seeds
|
|
35
|
+
|
|
36
|
+
**A web seed is an HTTP URL baked into the torrent that serves the same bytes** (BEP 19,
|
|
37
|
+
stored as `url-list`). Peers fetch from the swarm when peers exist and fall back to HTTP
|
|
38
|
+
when they do not.
|
|
39
|
+
|
|
40
|
+
For map distribution this solves the problem that otherwise makes a new torrent useless:
|
|
41
|
+
a freshly published archive has zero seeders, so without a web seed nobody can get it
|
|
42
|
+
until somebody already has it. With one, it works from the moment it is published and
|
|
43
|
+
simply gets cheaper as peers appear.
|
|
44
|
+
|
|
45
|
+
Web seeds are added automatically wherever the source implies one:
|
|
46
|
+
|
|
47
|
+
- **Adding from a URL** — the origin is registered by construction, since it serves the
|
|
48
|
+
exact bytes being hashed.
|
|
49
|
+
- **Watch folders** — set `webSeedBase` on the folder and each imported archive gets
|
|
50
|
+
`<webSeedBase>/<filename>`.
|
|
51
|
+
- **Explicitly** — pass `webSeeds` when adding.
|
|
52
|
+
|
|
53
|
+
They land in both the `.torrent` and the magnet:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
urlList in .torrent : ["https://maps.example.org/files/planet.pmtiles"]
|
|
57
|
+
magnet : magnet:?xt=urn:btih:333d...&dn=planet.pmtiles&ws=https%3A%2F%2F...
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If your archives are already on a web server or in S3, always pass the URL. It costs
|
|
61
|
+
nothing and turns cold start from a dead end into an HTTP fallback.
|
|
62
|
+
|
|
63
|
+
## Adding from a URL: keep or discard
|
|
64
|
+
|
|
65
|
+
Piece hashes are computed over content, so **there is no way to create a torrent from a
|
|
66
|
+
remote archive without reading every byte of it**. What you choose is whether those bytes
|
|
67
|
+
are kept.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Keep (default): downloads to savePath, node becomes a seeder
|
|
71
|
+
curl -X POST localhost:8090/api/torrents -H 'content-type: application/json' \
|
|
72
|
+
-d '{"url": "https://maps.example.org/files/planet.pmtiles"}'
|
|
73
|
+
|
|
74
|
+
# Discard: hashes in a single pass, keeps nothing
|
|
75
|
+
curl -X POST localhost:8090/api/torrents -H 'content-type: application/json' \
|
|
76
|
+
-d '{"url": "https://maps.example.org/files/planet.pmtiles", "retain": false}'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| | Disk | Bandwidth | Result |
|
|
80
|
+
| --- | --- | --- | --- |
|
|
81
|
+
| **keep** (default) | full archive | full archive, once | A real seeder immediately |
|
|
82
|
+
| **discard** | none | full archive, once | A torrent this node cannot seed |
|
|
83
|
+
|
|
84
|
+
Discard is legitimate when you already host the archive and only want to publish a
|
|
85
|
+
torrent for it — the web seed carries the swarm until a peer completes a copy. But it is
|
|
86
|
+
a poor default, because a torrent nobody seeds is HTTP with extra steps.
|
|
87
|
+
|
|
88
|
+
Either way, expect this to take as long as transferring the archive once. Progress is
|
|
89
|
+
logged as `[fetch] <url> NN%`.
|
|
90
|
+
|
|
91
|
+
## Piece size
|
|
92
|
+
|
|
93
|
+
Read amplification is `pieceLength ÷ bytesWanted`. Creation tools size pieces for
|
|
94
|
+
whole-file downloads, so large archives commonly end up at 16 MiB — where a cold 4 KB
|
|
95
|
+
vector tile costs a 16 MiB download.
|
|
96
|
+
|
|
97
|
+
The default here is **4 MiB**, trading a larger hash list for a quarter of the
|
|
98
|
+
amplification. Below about 1 MiB the hash list itself — which peers must transfer via
|
|
99
|
+
BEP 9 before any tile can be served — starts to dominate.
|
|
100
|
+
|
|
101
|
+
It is overridable at three levels, so the default never boxes you in:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
// globally
|
|
105
|
+
{ "pieceLength": 4194304 }
|
|
106
|
+
|
|
107
|
+
// per scheduled source
|
|
108
|
+
{ "sources": [{ "name": "…", "pieceLength": 16777216 }] }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# per request
|
|
113
|
+
curl -X POST localhost:8090/api/torrents -H 'content-type: application/json' \
|
|
114
|
+
-d '{"path": "/mnt/maps/planet.pmtiles", "pieceLength": 16777216}'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This only matters if the archive will be read *randomly*, as a tile server does. An
|
|
118
|
+
archive that will only ever be downloaded whole is fine with the 16 MiB other tools
|
|
119
|
+
default to, and pays a smaller hash list for it.
|
|
120
|
+
|
|
121
|
+
### Piece size and network equipment
|
|
122
|
+
|
|
123
|
+
Larger pieces are sometimes assumed to be gentler on routers. They mostly are not:
|
|
124
|
+
**peers request data in 16 KiB blocks regardless of piece size**, so packet volume for the
|
|
125
|
+
same bytes transferred is identical at 4 MiB and 16 MiB. What smaller pieces genuinely
|
|
126
|
+
increase is the hash list, per-piece bookkeeping in the client, and `HAVE` message
|
|
127
|
+
frequency — one per completed piece per peer, at nine bytes each.
|
|
128
|
+
|
|
129
|
+
What actually strains consumer hardware is the number of **simultaneous connections**,
|
|
130
|
+
because each is a NAT table entry and cheap routers exhaust those long before bandwidth
|
|
131
|
+
becomes the limit. That is a different setting:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{ "maxConnections": 100 }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Lower it — 50, or 30 — if the network misbehaves while seeding. It applies to both the
|
|
138
|
+
libtorrent and WebTorrent engines. Reach for that before compromising on piece size.
|
|
139
|
+
|
|
140
|
+
## BitTorrent v2
|
|
141
|
+
|
|
142
|
+
The `libtorrent` engine creates **hybrid v1+v2 torrents** by default. v2 (BEP 52) adds
|
|
143
|
+
per-file merkle trees with 16 KiB leaf blocks, so a peer can verify a small block without
|
|
144
|
+
holding the entire hash list — which matters precisely for the random-access reads a tile
|
|
145
|
+
server does. The v1 half keeps every existing client working.
|
|
146
|
+
|
|
147
|
+
Neither `mktorrent` nor `create-torrent` can produce these; only libtorrent can. Verified
|
|
148
|
+
against libtorrent 2.0.13, the same archive yields three distinct torrents:
|
|
149
|
+
|
|
150
|
+
| Format | `.torrent` size |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `hybrid` (default) | 415 B — carries both hash sets |
|
|
153
|
+
| `v1` | 274 B |
|
|
154
|
+
| `v2` | 371 B |
|
|
155
|
+
|
|
156
|
+
## Watch folders
|
|
157
|
+
|
|
158
|
+
A new `.pmtiles` appearing in a watched folder is imported automatically.
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"watch": [
|
|
163
|
+
{
|
|
164
|
+
"path": "/mnt/maps/generated",
|
|
165
|
+
"category": "basemaps",
|
|
166
|
+
"webSeedBase": "https://maps.example.org/files",
|
|
167
|
+
"stabilitySeconds": 30
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`stabilitySeconds` is the important one. A map build writes its output over minutes or
|
|
174
|
+
hours, and hashing a half-written archive produces a torrent for bytes that no longer
|
|
175
|
+
exist. Nothing is imported until the file has stopped changing for that long. Raise it if
|
|
176
|
+
your archives arrive over a network copy that can stall mid-file.
|
|
177
|
+
|
|
178
|
+
## Scheduled upstreams
|
|
179
|
+
|
|
180
|
+
Some upstreams publish a **new URL per build** rather than overwriting one. Protomaps does
|
|
181
|
+
this: `https://build.protomaps.com/20260806.pmtiles`, a new date every day, with older
|
|
182
|
+
builds left exactly as they were.
|
|
183
|
+
|
|
184
|
+
Watching for change never fires on that — nothing changes. What is needed is to work out
|
|
185
|
+
today's URL and see whether it exists yet:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"sourceCheckIntervalHours": 6,
|
|
190
|
+
"sources": [
|
|
191
|
+
{
|
|
192
|
+
"name": "planetiler-protomaps",
|
|
193
|
+
"url": "https://build.protomaps.com/{YYYYMMDD}.pmtiles",
|
|
194
|
+
"filename": "planetiler-protomaps-{YYYYMMDD}.pmtiles",
|
|
195
|
+
"offsetDays": -1,
|
|
196
|
+
"lookbackDays": 3,
|
|
197
|
+
"savePath": "/mnt/hd-16TB/store/generated/protomaps",
|
|
198
|
+
"latestLink": "planetiler-protomaps-latest.pmtiles",
|
|
199
|
+
"category": "planet",
|
|
200
|
+
"comment": "Planetiler protomaps data export",
|
|
201
|
+
"pieceLength": 4194304
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`url`
|
|
208
|
+
Template with `{YYYYMMDD}`, `{YYYY-MM-DD}`, `{YYYY}`, `{MM}` or `{DD}`.
|
|
209
|
+
|
|
210
|
+
`filename`
|
|
211
|
+
What to call it locally. Upstreams often publish under a bare date; this renames it to
|
|
212
|
+
something self-describing without touching the URL.
|
|
213
|
+
|
|
214
|
+
`offsetDays`
|
|
215
|
+
Most builds land the day after the date they are named for; `-1` looks for yesterday.
|
|
216
|
+
|
|
217
|
+
`lookbackDays`
|
|
218
|
+
Also check the preceding days. Without it, a poll missed while the daemon was down
|
|
219
|
+
loses that build permanently.
|
|
220
|
+
|
|
221
|
+
`latestLink`
|
|
222
|
+
A symlink pointing at the newest build. The dated file stays the real one, so it
|
|
223
|
+
remains seedable under its own torrent while consumers can reference a fixed path.
|
|
224
|
+
|
|
225
|
+
Each build becomes its own archive with its own torrent and its own lifetime, which is
|
|
226
|
+
what you want — old builds stay seedable for as long as anyone still wants them.
|
|
227
|
+
|
|
228
|
+
The origin URL is registered as a web seed automatically, and every candidate URL is
|
|
229
|
+
checked with a HEAD, so a build that has not been published yet costs one request.
|
|
230
|
+
|
|
231
|
+
## When the source changes underneath you
|
|
232
|
+
|
|
233
|
+
A torrent describes a fixed set of bytes. If the file it was built from is later
|
|
234
|
+
replaced — a nightly build overwriting `planet-latest.pmtiles` — the torrent does not
|
|
235
|
+
become invalid, but three things go wrong:
|
|
236
|
+
|
|
237
|
+
1. The catalog advertises content the source no longer has.
|
|
238
|
+
2. **Any web seed pointing at that source now serves bytes that fail hash
|
|
239
|
+
verification.** Peers waste bandwidth on it and eventually ban it.
|
|
240
|
+
3. If you were seeding from that file, the local copy no longer matches the torrent
|
|
241
|
+
either, so your node stops being a useful seeder.
|
|
242
|
+
|
|
243
|
+
Checking for this is cheap — one HEAD request per HTTP source, one `stat` per local file,
|
|
244
|
+
comparing ETag, Last-Modified and length. Nothing re-reads the archive.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Check one archive
|
|
248
|
+
curl -X POST localhost:8090/api/torrents/<infohash>/check
|
|
249
|
+
|
|
250
|
+
# Check everything with a source worth watching
|
|
251
|
+
curl -X POST localhost:8090/api/check-origins
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Or run it periodically:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{ "originCheckIntervalSeconds": 3600 }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
It defaults to off, because a node that only joins other people's torrents has nothing to
|
|
261
|
+
check. A changed source marks the entry `stale` with the reason and logs a warning.
|
|
262
|
+
|
|
263
|
+
To publish the new content:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
curl -X POST localhost:8090/api/torrents/<infohash>/rebuild
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Rebuilding automatically
|
|
270
|
+
|
|
271
|
+
Rebuilds can also happen on their own, but it is opt-in and guarded, because an
|
|
272
|
+
unattended rebuild re-hashes the archive — and for a remote source re-downloads it.
|
|
273
|
+
Hours of transfer started by nobody is not a good default.
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"originCheckIntervalSeconds": 3600,
|
|
278
|
+
"autoRebuild": {
|
|
279
|
+
"enabled": true,
|
|
280
|
+
"sources": ["file"],
|
|
281
|
+
"maxBytes": 53687091200,
|
|
282
|
+
"stabilitySeconds": 300
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`sources`
|
|
288
|
+
Which source types may be rebuilt unattended. Defaults to `["file"]` only: a local
|
|
289
|
+
rebuild costs a disk read, where adding `"http"` means re-downloading the entire
|
|
290
|
+
archive every time the origin changes.
|
|
291
|
+
|
|
292
|
+
`maxBytes`
|
|
293
|
+
Skip anything larger. Default 50 GiB; `0` disables the cap. This is the guard that
|
|
294
|
+
stops a planet archive from quietly consuming a day of I/O.
|
|
295
|
+
|
|
296
|
+
`stabilitySeconds`
|
|
297
|
+
The source must be unchanged for this long first. A build still writing its output
|
|
298
|
+
would otherwise be hashed mid-write — the same hazard watch folders guard against. If
|
|
299
|
+
the source moves again during the wait, the rebuild is deferred to the next check.
|
|
300
|
+
|
|
301
|
+
Rebuilds are serialised, so a sweep that finds five changed archives does not start five
|
|
302
|
+
concurrent multi-hour hashes.
|
|
303
|
+
|
|
304
|
+
### Guarding against false positives
|
|
305
|
+
|
|
306
|
+
A modification time that moves while size and ETag stay put is weak evidence — a touch, a
|
|
307
|
+
restored backup, or a re-upload of identical bytes all do it. Before calling that a
|
|
308
|
+
change, the archive's own PMTiles header is re-read and compared against the stored
|
|
309
|
+
summary: tile count, zoom range, format and bounds. That costs a few kilobytes even
|
|
310
|
+
against a multi-terabyte archive, and identical values mean the bytes are almost certainly
|
|
311
|
+
the same.
|
|
312
|
+
|
|
313
|
+
This matters most with auto-rebuild on, where the cost of a false positive is a needless
|
|
314
|
+
re-hash rather than a spurious warning.
|
|
315
|
+
|
|
316
|
+
### What it looks like
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
[origin] planet.pmtiles no longer matches its source (last-modified … -> …; size 468 -> 466).
|
|
320
|
+
The torrent is still valid, but its web seed will now fail hash verification for peers.
|
|
321
|
+
[rebuild] planet.pmtiles: waiting 2s for the source to settle
|
|
322
|
+
[rebuild] planet.pmtiles: rebuilding from /mnt/maps/planet.pmtiles
|
|
323
|
+
[rebuild] planet.pmtiles: b8ee8216… -> 7afd1ea1…
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The old entry stays in the catalog marked `stale` and `superseded`, with `supersededBy`
|
|
327
|
+
naming its replacement, so anything still seeding it keeps working while subscribers move
|
|
328
|
+
across via the feed.
|
|
329
|
+
|
|
330
|
+
This mints a **new infohash** — content-addressing means there is no other possibility.
|
|
331
|
+
The old entry is kept and marked `superseded`, with `supersededBy` naming the replacement,
|
|
332
|
+
so anything still seeding the old torrent keeps working while subscribers move across via
|
|
333
|
+
the feed. A byte-identical rebuild produces the same infohash and changes nothing.
|
|
334
|
+
|
|
335
|
+
### A fixed URL that gets rewritten
|
|
336
|
+
|
|
337
|
+
The other upstream shape: one permanent URL whose contents are replaced. Mapterhorn's
|
|
338
|
+
planet raster-DEM does this — `https://download.mapterhorn.com/planet.pmtiles`, 657 GiB,
|
|
339
|
+
rewritten when rebuilt. This is exactly what origin checking is for:
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
{
|
|
343
|
+
"originCheckIntervalSeconds": 21600,
|
|
344
|
+
"autoRebuild": { "enabled": true, "sources": ["http"], "maxBytes": 0 }
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
It serves a strong S3 ETag and a `Last-Modified`, so a change is detected in one HEAD
|
|
349
|
+
request. Note two things before enabling auto-rebuild for it:
|
|
350
|
+
|
|
351
|
+
- **`maxBytes` must be raised or zeroed.** At 657 GiB it is far over the 50 GiB default,
|
|
352
|
+
so a rebuild would otherwise be skipped — which is the default behaving correctly.
|
|
353
|
+
- **A rebuild transfers 657 GiB.** Hashing is over content, so there is no cheaper way,
|
|
354
|
+
and it happens again on every upstream rebuild. Consider `"retain": false` so the
|
|
355
|
+
transfer is a single streaming pass with no disk cost, leaning on the web seed — the
|
|
356
|
+
origin is behind a CDN and serves `Accept-Ranges: bytes`, so it makes a good one.
|
|
357
|
+
|
|
358
|
+
## Do not overwrite archives in place
|
|
359
|
+
|
|
360
|
+
If you are seeding an archive, replacing it in place breaks the torrent — the file no
|
|
361
|
+
longer matches the piece hashes, and your seeder silently stops being able to serve it.
|
|
362
|
+
|
|
363
|
+
Names like `planet-latest.pmtiles` invite exactly this. Prefer dated or versioned
|
|
364
|
+
filenames:
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
planetiler-openmaptiles-260615.pmtiles ← seedable forever
|
|
368
|
+
planetiler-openmaptiles-260713.pmtiles ← new build, new torrent
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
and, if you want a stable public URL, point `latest` at the newest as a **symlink or HTTP
|
|
372
|
+
redirect** rather than a file that gets rewritten. Each build then becomes its own torrent
|
|
373
|
+
with its own lifetime, old ones stay seedable for as long as anyone wants them, and the
|
|
374
|
+
feed (or a BEP 46 record) is what tells subscribers which is current.
|
|
375
|
+
|
|
376
|
+
## Rebuilt archives
|
|
377
|
+
|
|
378
|
+
A rebuilt archive is a **different torrent** — the infohash is a hash of the content, so
|
|
379
|
+
there is no such thing as updating one in place. Two ways to carry subscribers across a
|
|
380
|
+
rebuild, which fail differently, so publishing both is cheap insurance:
|
|
381
|
+
|
|
382
|
+
- **RSS** — easy to consume, understood by existing clients, needs a server that stays up.
|
|
383
|
+
- **BEP 46** — an ed25519-signed DHT record naming the current infohash, addressed by
|
|
384
|
+
public key rather than infohash (`magnet:?xs=urn:btpk:…`). No server required, but the
|
|
385
|
+
record expires and must be republished.
|
|
386
|
+
|
|
387
|
+
See [subscribing.md](subscribing.md).
|