pmtiles-swarm 0.33.0 β 0.35.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 +83 -12
- package/README.md +1 -1
- package/docs/architecture-diagram.md +50 -12
- package/docs/configuration.md +32 -5
- package/docs/internals.md +29 -0
- package/docs/publishing.md +25 -0
- package/package.json +2 -2
- package/src/api.js +65 -33
- package/src/config.js +11 -0
- package/src/index.js +11 -3
- package/src/library.js +264 -58
- package/src/torrent-create.js +107 -3
- package/src/web/index.html +65 -14
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,89 @@
|
|
|
7
7
|
### π Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.35.0
|
|
11
|
+
### β¨ Features and improvements
|
|
12
|
+
- **Requires pmtiles-torrent 0.7.0, which stops the libtorrent sidecar going deaf while it works.**
|
|
13
|
+
Its request loop ran each call to completion before reading the next, so a long one starved
|
|
14
|
+
everything behind it. Two of them are long. `create` hashes a whole archive, which for a 698 GiB
|
|
15
|
+
local add is hours β reported here as `libtorrent list timed out after 60000ms` every minute, a
|
|
16
|
+
console header stuck at "connectingβ¦", and archive details that never loaded. `read_piece` waits
|
|
17
|
+
up to 60s for a piece to arrive from the swarm, and every tile served from a cache-mode archive
|
|
18
|
+
goes through one, so a serving node spent most of its life unable to answer anything else.
|
|
19
|
+
|
|
20
|
+
Both now run off that loop. The second needed alert delivery reworked to a single pump with
|
|
21
|
+
subscribers first, because every consumer used to drain the session's one alert queue β so two
|
|
22
|
+
concurrent reads would have swallowed each other's `read_piece_alert` and both timed out. A read
|
|
23
|
+
also matches its alert on the torrent now, not just the piece number, and a storage fault is
|
|
24
|
+
reported once by the pump rather than only when a read happened to be waiting to notice it.
|
|
25
|
+
|
|
26
|
+
**Upgrade both together.** This release does not itself require the new behaviour, but it is the
|
|
27
|
+
version that asks for it, and a node running a 0.6.x sidecar keeps the stalls.
|
|
28
|
+
|
|
29
|
+
### π Bug fixes
|
|
30
|
+
|
|
31
|
+
## 0.34.0
|
|
32
|
+
### β¨ Features and improvements
|
|
33
|
+
- **`md5` is a declared setting.** It was already honoured wherever a torrent is created, but it
|
|
34
|
+
appeared in no defaults list and no document, so it could only be written into the config file by
|
|
35
|
+
hand β `PATCH /api/config` refused it as an unknown setting, and nothing in the console showed
|
|
36
|
+
whether it was on. It now defaults to `false`, is documented, and can be changed without a
|
|
37
|
+
restart.
|
|
38
|
+
- **`incomingRetentionDays`** sets how long an unfinished download stays resumable. Defaults to 14.
|
|
39
|
+
|
|
40
|
+
### π Bug fixes
|
|
41
|
+
- **A large download survives a restart instead of starting again from zero.** A scheduled web
|
|
42
|
+
source fetching a multi-hour archive lost the whole transfer every time the node restarted, and
|
|
43
|
+
began again from nothing on the next poll. Three things had to hold and only one did. The bytes
|
|
44
|
+
were always kept β the staging directory is named for a hash of its URL so the next add finds
|
|
45
|
+
it β but **shutdown deleted them**, because it stopped in-flight adds through `cancelAdd()`, and
|
|
46
|
+
cancelling discards the partial on purpose: somebody said stop. A restart is not that decision,
|
|
47
|
+
so shutdown now uses `stopAdds()`, which the fetch can tell apart. **Startup then swept whatever
|
|
48
|
+
survived**, on the reasoning that a killed process leaves a partial "nothing will ever look in
|
|
49
|
+
again" β true when staging names were random, false since they became a hash of the URL. And
|
|
50
|
+
**the validator did not outlive the process**: the `ETag` a resume is checked against lived in a
|
|
51
|
+
local, so a new process had nothing to compare and refused the resume as "the server offers no
|
|
52
|
+
ETag or Last-Modified", deleting the partial by the very attempt meant to continue it. It is now
|
|
53
|
+
written beside the bytes and removed when the download completes. A restart during a 700 GiB
|
|
54
|
+
transfer now costs the seconds since the last write.
|
|
55
|
+
- **`.incoming` is swept by age rather than emptied.** Only a staging directory nothing has
|
|
56
|
+
written to for `incomingRetentionDays` (default 14) is cleared, so an unfinished download stays
|
|
57
|
+
resumable. The sweep also looks under `cacheSavePath`, which it never did β staging lands there
|
|
58
|
+
for cache-mode adds and under a source's own `savePath`, so the one configured `savePath` was
|
|
59
|
+
never the whole of where it could be.
|
|
60
|
+
- **Adding a local archive answers when the file has been checked, not when it has been hashed.**
|
|
61
|
+
`POST /api/torrents` with a `path` held the response open for the whole hash β every byte of the
|
|
62
|
+
archive, twice with `md5` on β so the console's add dialog sat there for minutes with no sign
|
|
63
|
+
that anything was happening. Worse than the URL case it mirrors, because nothing was downloading
|
|
64
|
+
either: the file was already on the disk and visibly not moving, which reads as a submit button
|
|
65
|
+
that did nothing. It now answers `202` once the path has been identified and accepted, and the
|
|
66
|
+
hash reports itself through `/api/adds` like a download does. A path that is not there or is not
|
|
67
|
+
an archive still fails in the response. An archive already held answers `200` with its entry,
|
|
68
|
+
which the URL branch now does too rather than promising work that was already done. **Scripts
|
|
69
|
+
reading the created entry straight back from a `path` add need `/api/adds` or `/api/torrents`
|
|
70
|
+
instead** β magnets and `.torrent` URLs are unchanged and still answer `201`.
|
|
71
|
+
- **A second add of a file already being hashed joins the first rather than starting its own.**
|
|
72
|
+
Only reachable now that the dialog closes quickly enough to submit twice, and two passes over the
|
|
73
|
+
same planet archive is an hour of disk for one result.
|
|
74
|
+
- **The console's MD5 checkbox is now the decision it looks like.** It was only sent when ticked,
|
|
75
|
+
and the server reads an absent `md5` as "unspecified" and falls back to the node's configured
|
|
76
|
+
default β so on a node with `md5` on, an unticked box still hashed one, and the log said so while
|
|
77
|
+
the dialog appeared to have turned it off. The value is sent either way, and the box now starts
|
|
78
|
+
from the node's own setting rather than always unticked β otherwise making it authoritative would
|
|
79
|
+
have turned a configured default off for every add made from the console, the same disagreement
|
|
80
|
+
the other way round. Omitting `md5` from an API or CLI call still inherits the config default,
|
|
81
|
+
which is what that fallback is for.
|
|
82
|
+
- **The save-location picker is hidden when adding a local file.** It did nothing there: a local
|
|
83
|
+
add registers the file's own directory as the save path whatever was chosen, which is exactly
|
|
84
|
+
what "hashed in place, nothing is copied" says β but the picker sat next to that sentence
|
|
85
|
+
implying otherwise, and offered no way to say "leave it where it is" because that is the only
|
|
86
|
+
thing it does.
|
|
87
|
+
- **"What a torrent-aware client does" describes what they now do.** The section predated the swarm
|
|
88
|
+
handles moving into the TileJSON URL's fragment and still had a client learning where to join
|
|
89
|
+
from a TileJSON response β the one thing the fragment exists to avoid, since the swarm is the
|
|
90
|
+
part that depends on no server. It contradicted "bootstrapping without the server" two sections
|
|
91
|
+
below it.
|
|
92
|
+
|
|
10
93
|
## 0.33.0
|
|
11
94
|
### β¨ Features and improvements
|
|
12
95
|
- **Requires pmtiles-torrent 0.6.1, which is what finally makes a downloading archive servable.**
|
|
@@ -21,7 +104,6 @@
|
|
|
21
104
|
|
|
22
105
|
## 0.32.3
|
|
23
106
|
### β¨ Features and improvements
|
|
24
|
-
- _...Add new stuff here..._
|
|
25
107
|
|
|
26
108
|
### π Bug fixes
|
|
27
109
|
- **Requires pmtiles-torrent 0.5.2, which deletes a torrent's resume file with its data.** Resume
|
|
@@ -33,11 +115,9 @@
|
|
|
33
115
|
the data goes too: a removal that keeps the files is how a pause is expressed for an engine with
|
|
34
116
|
no pause of its own, and discarding resume data there would turn every pause into a full re-hash.
|
|
35
117
|
|
|
36
|
-
- _...Add new stuff here..._
|
|
37
118
|
|
|
38
119
|
## 0.32.2
|
|
39
120
|
### β¨ Features and improvements
|
|
40
|
-
- _...Add new stuff here..._
|
|
41
121
|
|
|
42
122
|
### π Bug fixes
|
|
43
123
|
- **The head warmer no longer skips every archive it was built for.** It decided an archive was done
|
|
@@ -56,11 +136,9 @@
|
|
|
56
136
|
header off local disk for nothing, while assuming the opposite would leave every existing
|
|
57
137
|
subscription stuck exactly as it is.
|
|
58
138
|
|
|
59
|
-
- _...Add new stuff here..._
|
|
60
139
|
|
|
61
140
|
## 0.32.1
|
|
62
141
|
### β¨ Features and improvements
|
|
63
|
-
- _...Add new stuff here..._
|
|
64
142
|
|
|
65
143
|
### π Bug fixes
|
|
66
144
|
- **Requires pmtiles-torrent 0.5.1, so the connection indicator and Recheck files actually work.**
|
|
@@ -70,11 +148,9 @@
|
|
|
70
148
|
cannot answer is deliberately not reported as unreachable, so there was nothing to see and
|
|
71
149
|
nothing to explain why.
|
|
72
150
|
|
|
73
|
-
- _...Add new stuff here..._
|
|
74
151
|
|
|
75
152
|
## 0.32.0
|
|
76
153
|
### β¨ Features and improvements
|
|
77
|
-
- _...Add new stuff here..._
|
|
78
154
|
|
|
79
155
|
### π Bug fixes
|
|
80
156
|
- **A mutable magnet no longer carries a web seed.** A `ws=` URL names one build; a BEP 46 magnet
|
|
@@ -100,7 +176,6 @@
|
|
|
100
176
|
**Restyle anything holding one.** A style carrying an older mutable magnet keeps working, but
|
|
101
177
|
carries the stale web seed until it is regenerated.
|
|
102
178
|
|
|
103
|
-
- _...Add new stuff here..._
|
|
104
179
|
|
|
105
180
|
## 0.31.0
|
|
106
181
|
### β¨ Features and improvements
|
|
@@ -127,10 +202,8 @@
|
|
|
127
202
|
With two engines both are asked, since each keeps its own belief about the same file and a stale
|
|
128
203
|
one on the secondary is why a browser peer would find nothing while the primary seeds happily.
|
|
129
204
|
|
|
130
|
-
- _...Add new stuff here..._
|
|
131
205
|
|
|
132
206
|
### π Bug fixes
|
|
133
|
-
- _...Add new stuff here..._
|
|
134
207
|
|
|
135
208
|
## 0.30.0
|
|
136
209
|
### β¨ Features and improvements
|
|
@@ -160,10 +233,8 @@
|
|
|
160
233
|
makes the eye stop to work out which it is looking at. Seconds are dropped rather than the date,
|
|
161
234
|
since nothing here is sorted finely enough for them to matter; hovering still gives them.
|
|
162
235
|
|
|
163
|
-
- _...Add new stuff here..._
|
|
164
236
|
|
|
165
237
|
### π Bug fixes
|
|
166
|
-
- _...Add new stuff here..._
|
|
167
238
|
|
|
168
239
|
## 0.29.0
|
|
169
240
|
### β¨ Features and improvements
|
package/README.md
CHANGED
|
@@ -761,7 +761,7 @@ which the endpoint answers 501.
|
|
|
761
761
|
| `POST` | `/api/torrents/:infoHash/check` | Has the source changed since the torrent was made? |
|
|
762
762
|
| `POST` | `/api/torrents/:infoHash/rebuild` | Rebuild from the current source (mints a new infohash) |
|
|
763
763
|
| `POST` | `/api/check-origins` | Check every archive with a watchable source |
|
|
764
|
-
| `GET` `DELETE` | `/api/adds` |
|
|
764
|
+
| `GET` `DELETE` | `/api/adds` | Adds still in flight β downloads, and local files being hashed β and cancelling a download by URL. A hash cannot be stopped part-way, so it is listed but not cancellable |
|
|
765
765
|
| `GET` `POST` | `/api/speed` | Which speed limits are in force, and the manual switch between the two sets |
|
|
766
766
|
| `GET` | `/api/categories` | Every category, with the endpoints resolving to its newest build |
|
|
767
767
|
| `POST` | `/api/adopt`, `/api/adopt/candidates` | Take over what an engine or another node holds |
|
|
@@ -154,6 +154,15 @@ stops needing the tile endpoint at all.
|
|
|
154
154
|
The important part is that it does both at once β HTTP for the first paint, swarm
|
|
155
155
|
in the background β so there is never a blank map waiting for metadata.
|
|
156
156
|
|
|
157
|
+
Discovery comes first, and it comes from the style rather than from a response.
|
|
158
|
+
The source URL carries its handles in a fragment
|
|
159
|
+
(`β¦/tiles.json#torrent=β¦&magnet=β¦`, described under
|
|
160
|
+
[bootstrapping](#bootstrapping-without-the-server)), so a client knows there is a
|
|
161
|
+
swarm behind a source before it makes a single request β which is the point, as
|
|
162
|
+
that is what still works when the server is down. The TileJSON is fetched anyway,
|
|
163
|
+
because it carries the tile endpoint for the first paint and a fuller `torrent`
|
|
164
|
+
block than a fragment can, but it is no longer how the swarm is _found_.
|
|
165
|
+
|
|
157
166
|
```mermaid
|
|
158
167
|
sequenceDiagram
|
|
159
168
|
autonumber
|
|
@@ -162,24 +171,28 @@ sequenceDiagram
|
|
|
162
171
|
participant HTTP as pmtiles-swarm<br/>(via CDN)
|
|
163
172
|
participant BT as BitTorrent swarm
|
|
164
173
|
|
|
165
|
-
App->>P: load style
|
|
166
|
-
P
|
|
167
|
-
HTTP-->>P: TileJSON + torrent block
|
|
168
|
-
|
|
169
|
-
Note over P: claims the /archives/{hash}/ prefix,<br/>so only these URLs come to it
|
|
174
|
+
App->>P: load style
|
|
175
|
+
Note over P: reads the fragment on each source URL:<br/>torrent= and magnet=. No request yet.
|
|
170
176
|
|
|
171
177
|
par Map is usable immediately
|
|
178
|
+
P->>HTTP: GET /latest/{category}/tiles.json
|
|
179
|
+
Note over P,HTTP: the fragment is never sent
|
|
180
|
+
HTTP-->>P: TileJSON + torrent block
|
|
172
181
|
App->>P: tile 12/2145/1436
|
|
173
182
|
P->>HTTP: GET β¦/12/2145/1436.pbf
|
|
174
183
|
HTTP-->>App: tile bytes
|
|
175
184
|
and Swarm warms up in the background
|
|
176
|
-
P->>
|
|
185
|
+
P->>HTTP: GET the .torrent
|
|
186
|
+
HTTP-->>P: metainfo (piece hashes, trackers, web seeds)
|
|
187
|
+
P->>BT: join β metadata already in hand
|
|
177
188
|
BT-->>P: connected
|
|
178
189
|
P->>BT: fetch PMTiles header + root directory
|
|
179
190
|
BT-->>P: those pieces
|
|
180
191
|
Note over P: now able to resolve any tile<br/>to a byte range locally
|
|
181
192
|
end
|
|
182
193
|
|
|
194
|
+
Note over P: claims the TileJSON URL's prefix (without<br/>the fragment), so only these URLs come to it
|
|
195
|
+
|
|
183
196
|
App->>P: tile 12/2146/1436
|
|
184
197
|
Note over P: tile β byte range (PMTiles directory)<br/>β piece index
|
|
185
198
|
P->>BT: fetch that piece
|
|
@@ -201,18 +214,43 @@ is why the `torrent` block carries the archive's `.torrent` rather than per-tile
|
|
|
201
214
|
URLs: **there is nothing tile-specific in the swarm.** The swarm holds one file,
|
|
202
215
|
and both ends know how to read tiles out of it.
|
|
203
216
|
|
|
204
|
-
|
|
217
|
+
Consequences worth being clear about:
|
|
205
218
|
|
|
206
219
|
- **HTTP is never fully abandoned.** It is the fallback for anything the swarm
|
|
207
220
|
cannot answer quickly, and the only path until the swarm is connected.
|
|
208
221
|
- **The client becomes a seeder.** Every piece it pulls, it serves β so a popular
|
|
209
222
|
region gets _faster_ as more clients view it, which is the opposite of how a
|
|
210
223
|
tile server behaves under load.
|
|
211
|
-
- **
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
- **The document is preferred, the fragment is the fallback.** Where the TileJSON
|
|
225
|
+
is reachable and carries a `torrent` block, that block wins: it is the archive's
|
|
226
|
+
own account of itself, and it has web seeds, size and piece length that two
|
|
227
|
+
handles in a URL do not. The fragment is what answers when the document does not
|
|
228
|
+
β unreachable, not JSON, or carrying no block β which is exactly the case the
|
|
229
|
+
handles were put in the URL for. A client that used only one of the two would be
|
|
230
|
+
either poorly informed or dependent on the server it is meant to survive.
|
|
231
|
+
- **Prefer the `.torrent` over the magnet, and for a browser this is not a
|
|
232
|
+
preference.** A magnet carries only an infohash, so the client must find peers
|
|
233
|
+
and complete a metadata exchange before it knows anything about the archive β
|
|
234
|
+
measured at 90 to 240 seconds against a 72 GiB archive. That is the cost for a
|
|
235
|
+
client with a DHT. A browser has neither DHT nor UDP, and piece hashes reach a
|
|
236
|
+
BitTorrent client only from a **peer**, over BEP 9 β a web seed serves file
|
|
237
|
+
payload and never metainfo. So a page that cannot reach a peer cannot use the
|
|
238
|
+
web seed either, however reachable that web seed is: it has bytes it is not
|
|
239
|
+
allowed to trust. Fetching the `.torrent` over the same HTTPS the TileJSON came
|
|
240
|
+
from takes the peer off the critical path entirely, which is the difference
|
|
241
|
+
between working on a restricted network and not working at all.
|
|
242
|
+
- **A client with a DHT can join on a magnet alone**, and one without cannot. That
|
|
243
|
+
is the one place the two kinds of client genuinely diverge, and it is why the
|
|
244
|
+
convention names `torrent=` first: a browser treats a source carrying only a
|
|
245
|
+
magnet as no candidate at all, while a native client is happy with it.
|
|
246
|
+
- **Holding the metainfo is not evidence that anything will serve it.** It makes
|
|
247
|
+
the engine ready with no peer involved, which removes the very thing that used
|
|
248
|
+
to prove an archive was worth binding a source to β waiting for metadata was
|
|
249
|
+
never only a wait. A client that registers on metainfo alone can bind a source
|
|
250
|
+
to a swarm with nothing behind it, and that does not fall back, it _stalls_, and
|
|
251
|
+
the tiles never draw. One `Range: bytes=0-0` against the web seed restores the
|
|
252
|
+
evidence: a 206 proves the host is reachable, serves ranges and permits the
|
|
253
|
+
origin, which is all the engine needs from it.
|
|
216
254
|
|
|
217
255
|
---
|
|
218
256
|
|
package/docs/configuration.md
CHANGED
|
@@ -215,11 +215,13 @@ seeding.
|
|
|
215
215
|
|
|
216
216
|
## Creating torrents
|
|
217
217
|
|
|
218
|
-
| setting
|
|
219
|
-
|
|
|
220
|
-
| `pieceLength`
|
|
221
|
-
| `torrentFormat`
|
|
222
|
-
| `allowUnknownArchives`
|
|
218
|
+
| setting | default | |
|
|
219
|
+
| ----------------------- | ---------- | ----------------------------------------------- |
|
|
220
|
+
| `pieceLength` | `4194304` | 4 MiB |
|
|
221
|
+
| `torrentFormat` | `'hybrid'` | `'hybrid'`, `'v1'` or `'v2'` |
|
|
222
|
+
| `allowUnknownArchives` | `false` | publish files not recognised as map archives |
|
|
223
|
+
| `md5` | `false` | also compute an MD5 of each archive created |
|
|
224
|
+
| `incomingRetentionDays` | `14` | how long an unfinished download stays resumable |
|
|
223
225
|
|
|
224
226
|
### `pieceLength`
|
|
225
227
|
|
|
@@ -260,6 +262,31 @@ reading over a swarm does not work the way it does for a flat, Hilbert-ordered
|
|
|
260
262
|
file; a finished local copy has no such problem. See
|
|
261
263
|
[serving-tiles.md](serving-tiles.md#what-can-be-served).
|
|
262
264
|
|
|
265
|
+
### `incomingRetentionDays`
|
|
266
|
+
|
|
267
|
+
An unfinished download is kept in `.incoming` for this long, and adding the same
|
|
268
|
+
URL again resumes it β which is what makes a restart during a multi-hour
|
|
269
|
+
transfer cost minutes rather than the whole download. A scheduled source picks
|
|
270
|
+
its own back up on the next poll without being asked.
|
|
271
|
+
|
|
272
|
+
Only what nothing has written to for this many days is cleared at startup, since
|
|
273
|
+
whether a URL is still wanted is a question about configuration that the sweep
|
|
274
|
+
cannot see. Set it lower on a small disk, or higher if an upstream can be
|
|
275
|
+
unreachable for weeks.
|
|
276
|
+
|
|
277
|
+
### `md5`
|
|
278
|
+
|
|
279
|
+
Off by default, because it costs a second full read of the archive: with it on,
|
|
280
|
+
adding a 700 GiB file takes twice as long and produces one convenience digest
|
|
281
|
+
that nothing in BitTorrent uses. The torrent already verifies the content, and
|
|
282
|
+
per piece rather than as a whole β this is for the manual check somebody wants to
|
|
283
|
+
run against a published checksum, and it is carried in the feed for them.
|
|
284
|
+
|
|
285
|
+
The console's **Also compute an MD5** box starts from this setting and is sent
|
|
286
|
+
with the add either way, so unticking it applies to that add alone. An API or CLI
|
|
287
|
+
call that omits `md5` inherits this; one that passes `true` or `false` decides
|
|
288
|
+
for itself.
|
|
289
|
+
|
|
263
290
|
## Trackers
|
|
264
291
|
|
|
265
292
|
`trackers` is baked into every torrent this node creates. It defaults to the
|
package/docs/internals.md
CHANGED
|
@@ -556,6 +556,35 @@ assumed:
|
|
|
556
556
|
|
|
557
557
|
Any of those failing restarts the download rather than guessing.
|
|
558
558
|
|
|
559
|
+
### Across a restart, not only across a stall
|
|
560
|
+
|
|
561
|
+
All of that worked within one process and none of it survived leaving one, which
|
|
562
|
+
made a restart during a long download cost the whole download. Three separate
|
|
563
|
+
things had to be true, and only the first was:
|
|
564
|
+
|
|
565
|
+
1. **The bytes are kept.** They always were β the staging directory is named for
|
|
566
|
+
`sha256(url)`, so the next add of the same URL finds it.
|
|
567
|
+
2. **Nothing deletes them on the way past.** Two things did. Shutdown expressed
|
|
568
|
+
itself through `cancelAdd()`, and cancelling deletes the partial on purpose β
|
|
569
|
+
somebody said stop. A restart is not that decision, so shutdown now calls
|
|
570
|
+
`stopAdds()`, which aborts with a reason the fetch can tell apart. Startup
|
|
571
|
+
then swept `.incoming` unconditionally, on the reasoning that a killed
|
|
572
|
+
process leaves a partial "nothing will ever look in again" β true when
|
|
573
|
+
staging names were random, false since they became a hash of the URL. It now
|
|
574
|
+
reaps only what nothing has written to for `incomingRetentionDays`.
|
|
575
|
+
3. **The validator outlives the process.** `stillTheSameFile` compares the
|
|
576
|
+
`ETag` seen when the download began against the one offered now, and that
|
|
577
|
+
header lived in a local. A new process had nothing to compare, so the resume
|
|
578
|
+
was refused for the one reason that cannot be recovered from β "the server
|
|
579
|
+
offers no ETag or Last-Modified" β and the partial was deleted by the very
|
|
580
|
+
attempt meant to continue it. It is now written to `<partial>.resume` beside
|
|
581
|
+
the bytes, carrying the URL it belongs to, and removed when the download
|
|
582
|
+
finishes so it cannot keep the staging directory from being cleared.
|
|
583
|
+
|
|
584
|
+
The URL is recorded in the sidecar as well as implied by the directory name, so
|
|
585
|
+
a staging directory that has been reused for something else is refused rather
|
|
586
|
+
than spliced.
|
|
587
|
+
|
|
559
588
|
## Retiring and pruning a subscription
|
|
560
589
|
|
|
561
590
|
Two different questions, which is why an RSS feed can have the first and not the
|
package/docs/publishing.md
CHANGED
|
@@ -54,6 +54,31 @@ curl -X POST localhost:8090/api/torrents \
|
|
|
54
54
|
curl -X POST localhost:8090/api/adopt
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
The first two of those answer differently, and a script should know which it is
|
|
58
|
+
reading. A local path and a URL both take as long as it takes to read every byte
|
|
59
|
+
of the archive β minutes for a local file, hours for a planet download, doubled
|
|
60
|
+
again if `md5` is on β so they answer **`202 Accepted`** the moment the source
|
|
61
|
+
has been checked, and the work carries on behind it:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"accepted": true,
|
|
66
|
+
"path": "/mnt/maps/planet.pmtiles",
|
|
67
|
+
"message": "hashing; progress is reported by /api/adds"
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
There is no infohash in that, because there is not one yet. `GET /api/adds`
|
|
72
|
+
lists what is still running, and the archive appears in `GET /api/torrents` once
|
|
73
|
+
it finishes. A source that fails its checks β a path that is not there, a URL
|
|
74
|
+
that does not answer, a file that is not an archive β fails in the response
|
|
75
|
+
instead, since that is what somebody can do something about. An archive already
|
|
76
|
+
in the catalog answers `200` with the existing entry.
|
|
77
|
+
|
|
78
|
+
A magnet, a `.torrent` URL and an uploaded `.torrent` are metadata rather than
|
|
79
|
+
data, so there is nothing slow to wait for: those still answer `201` with the
|
|
80
|
+
entry.
|
|
81
|
+
|
|
57
82
|
### When the source URL is not published
|
|
58
83
|
|
|
59
84
|
Adding from a URL registers that URL as a web seed by default, because it is by
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"maplibre-gl": "^6.2.0",
|
|
47
47
|
"parse-torrent": "^11.0.24",
|
|
48
48
|
"pmtiles": "^4.4.1",
|
|
49
|
-
"pmtiles-torrent": "^0.
|
|
49
|
+
"pmtiles-torrent": "^0.7.0",
|
|
50
50
|
"webtorrent": "^3.0.21"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
package/src/api.js
CHANGED
|
@@ -53,6 +53,55 @@ function route(handler) {
|
|
|
53
53
|
return (req, res, next) => Promise.resolve(handler(req, res)).catch(next);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Starts an add that takes minutes to hours, and answers as soon as it is safe
|
|
58
|
+
* to say it has been accepted.
|
|
59
|
+
*
|
|
60
|
+
* Everything a caller can do something about is known long before the work is
|
|
61
|
+
* finished: the source answers, and it is an archive of a kind this will
|
|
62
|
+
* publish. What remains is transfer and hashing. Awaiting all of it held the
|
|
63
|
+
* response open for the whole thing, so the console's add dialog stayed on
|
|
64
|
+
* screen for the duration β over an archive that was visibly appearing behind
|
|
65
|
+
* it in the URL case, and over a file that had never moved in the local one,
|
|
66
|
+
* where it read as a submit button that had done nothing at all.
|
|
67
|
+
*
|
|
68
|
+
* Progress has its own route already: `runningAdds()` feeds `/api/adds`, the
|
|
69
|
+
* console polls it, and `DELETE /api/adds` cancels the ones that can be.
|
|
70
|
+
* @param {object} res - The response to answer.
|
|
71
|
+
* @param {Function} start - Called with `{onValidated}`; returns the add's promise.
|
|
72
|
+
* @param {object} accepted - Fields describing the source, for the 202 body.
|
|
73
|
+
* @param {string} message - What the 202 tells the caller is now happening.
|
|
74
|
+
* @param {string} what - Prefixed log tag and source, for a failure nobody is waiting on.
|
|
75
|
+
* @returns {Promise<void>} - Resolves once the response has been sent.
|
|
76
|
+
*/
|
|
77
|
+
async function acceptAdd(res, { start, accepted, message, what }) {
|
|
78
|
+
const validated = Promise.withResolvers();
|
|
79
|
+
const running = start({ onValidated: validated.resolve })
|
|
80
|
+
// A failure after validation has nowhere to be reported: the response has
|
|
81
|
+
// gone. It is logged where the rest of the work is, and swallowed here so
|
|
82
|
+
// it cannot take the process down as an unhandled rejection.
|
|
83
|
+
.catch((error) => {
|
|
84
|
+
validated.reject(error);
|
|
85
|
+
console.error(`${what}: ${error.message}`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Whichever comes first: the checks passing, or the whole attempt failing. A
|
|
89
|
+
// source that does not answer, or is not an archive, still reports itself in
|
|
90
|
+
// the dialog where somebody can correct it.
|
|
91
|
+
const outcome = await validated.promise;
|
|
92
|
+
|
|
93
|
+
// Already held, so there is no work to wait on and the entry itself is the
|
|
94
|
+
// better answer. Deliberately not extended to `joined`, where the promise
|
|
95
|
+
// belongs to somebody else's transfer and awaiting it would hold the
|
|
96
|
+
// response open for exactly as long as this exists to avoid.
|
|
97
|
+
if (outcome?.held) {
|
|
98
|
+
return void res.status(200).json(await running);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
void running;
|
|
102
|
+
res.status(202).json({ accepted: true, ...accepted, message });
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
/**
|
|
57
106
|
* Builds the HTTP application: JSON API, RSS feeds and the web UI.
|
|
58
107
|
* @param {object} deps - Collaborators.
|
|
@@ -1481,41 +1530,24 @@ export function createApp({
|
|
|
1481
1530
|
|
|
1482
1531
|
let entry;
|
|
1483
1532
|
if (body.path) {
|
|
1484
|
-
|
|
1533
|
+
// Nothing is copied and nothing is downloaded, but every byte is still
|
|
1534
|
+
// read to compute the piece hashes β twice with md5 on. That is the
|
|
1535
|
+
// whole wait for a local add, and it is no shorter for the file being
|
|
1536
|
+
// on this disk already. See acceptAdd().
|
|
1537
|
+
return await acceptAdd(res, {
|
|
1538
|
+
start: (hooks) =>
|
|
1539
|
+
library.addLocalArchive(body.path, { ...options, ...hooks }),
|
|
1540
|
+
accepted: { path: body.path },
|
|
1541
|
+
message: 'hashing; progress is reported by /api/adds',
|
|
1542
|
+
what: `[hash] ${body.path}`,
|
|
1543
|
+
});
|
|
1485
1544
|
} else if (body.url) {
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
// that was visibly appearing behind it.
|
|
1491
|
-
//
|
|
1492
|
-
// Progress has its own route already: runningAdds() feeds /api/adds,
|
|
1493
|
-
// the console polls it, and DELETE /api/adds cancels one. This is the
|
|
1494
|
-
// piece that was missing rather than a new mechanism.
|
|
1495
|
-
const validated = Promise.withResolvers();
|
|
1496
|
-
const running = library
|
|
1497
|
-
.addRemoteArchive(body.url, {
|
|
1498
|
-
...options,
|
|
1499
|
-
onValidated: validated.resolve,
|
|
1500
|
-
})
|
|
1501
|
-
// A failure after validation has nowhere to be reported: the
|
|
1502
|
-
// response has gone. It is logged where the rest of the fetch is,
|
|
1503
|
-
// and swallowed here so it cannot take the process down as an
|
|
1504
|
-
// unhandled rejection.
|
|
1505
|
-
.catch((error) => {
|
|
1506
|
-
validated.reject(error);
|
|
1507
|
-
console.error(`[fetch] ${body.url}: ${error.message}`);
|
|
1508
|
-
});
|
|
1509
|
-
|
|
1510
|
-
// Whichever comes first: the checks passing, or the whole attempt
|
|
1511
|
-
// failing. A URL that does not answer, or is not an archive, still
|
|
1512
|
-
// reports itself in the dialog where somebody can correct it.
|
|
1513
|
-
await validated.promise;
|
|
1514
|
-
void running;
|
|
1515
|
-
return res.status(202).json({
|
|
1516
|
-
accepted: true,
|
|
1517
|
-
url: body.url,
|
|
1545
|
+
return await acceptAdd(res, {
|
|
1546
|
+
start: (hooks) =>
|
|
1547
|
+
library.addRemoteArchive(body.url, { ...options, ...hooks }),
|
|
1548
|
+
accepted: { url: body.url },
|
|
1518
1549
|
message: 'fetching; progress is reported by /api/adds',
|
|
1550
|
+
what: `[fetch] ${body.url}`,
|
|
1519
1551
|
});
|
|
1520
1552
|
} else if (body.magnet) {
|
|
1521
1553
|
entry = await library.addExistingTorrent(
|
package/src/config.js
CHANGED
|
@@ -155,6 +155,17 @@ const DEFAULTS = {
|
|
|
155
155
|
* publish any readable file to a public swarm.
|
|
156
156
|
*/
|
|
157
157
|
allowUnknownArchives: false,
|
|
158
|
+
/**
|
|
159
|
+
* Also compute an MD5 of each archive created here. Costs a second full read
|
|
160
|
+
* of the file. Already honoured wherever a torrent is created; declared so it
|
|
161
|
+
* can be seen and set rather than only written into the file by hand.
|
|
162
|
+
*/
|
|
163
|
+
md5: false,
|
|
164
|
+
/**
|
|
165
|
+
* How long an unfinished download is kept before startup treats it as
|
|
166
|
+
* abandoned. Until then, re-adding the same URL resumes it.
|
|
167
|
+
*/
|
|
168
|
+
incomingRetentionDays: 14,
|
|
158
169
|
/**
|
|
159
170
|
* Who may administer this node. Tiles, TileJSON and the feed are always
|
|
160
171
|
* public; everything under /api/ is gated whenever anything here is set. See
|
package/src/index.js
CHANGED
|
@@ -237,9 +237,17 @@ PMTILES_SWARM_PUBLIC_URL
|
|
|
237
237
|
stoppers.unshift({
|
|
238
238
|
label: 'downloads in progress',
|
|
239
239
|
stop: () => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
// stopAdds, not cancelAdd: a restart is not a decision to stop wanting
|
|
241
|
+
// the archive, and cancelling deletes the partial download. Through
|
|
242
|
+
// cancelAdd every restart threw away whatever was in flight, and the
|
|
243
|
+
// scheduled source that asked for it began again from zero on the next
|
|
244
|
+
// poll β which for a planet build is hours of transfer per restart.
|
|
245
|
+
const stopped = library.stopAdds();
|
|
246
|
+
if (stopped.length > 0) {
|
|
247
|
+
console.log(
|
|
248
|
+
`[shutdown] stopped ${stopped.length} download(s); their bytes are ` +
|
|
249
|
+
'kept and resume when the source is next polled',
|
|
250
|
+
);
|
|
243
251
|
}
|
|
244
252
|
},
|
|
245
253
|
ms: 1000,
|