pmtiles-swarm 0.30.0 β 0.32.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 +60 -0
- package/README.md +1 -0
- package/docs/engines.md +41 -3
- package/docs/serving-tiles.md +2 -2
- package/docs/tilejson.md +16 -1
- package/package.json +1 -1
- package/src/api.js +15 -1
- package/src/engines/composite.js +44 -0
- package/src/engines/libtorrent.js +27 -0
- package/src/engines/qbittorrent.js +14 -0
- package/src/engines/types.js +1 -0
- package/src/feed.js +0 -1
- package/src/library.js +62 -0
- package/src/mutable.js +19 -7
- package/src/publisher.js +0 -1
- package/src/tilejson.js +0 -1
- package/src/web/index.html +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,66 @@
|
|
|
7
7
|
### π Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.32.0
|
|
11
|
+
### β¨ Features and improvements
|
|
12
|
+
- _...Add new stuff here..._
|
|
13
|
+
|
|
14
|
+
### π Bug fixes
|
|
15
|
+
- **A mutable magnet no longer carries a web seed.** A `ws=` URL names one build; a BEP 46 magnet
|
|
16
|
+
names a series. They disagree the moment the next build is published β and not harmlessly, which
|
|
17
|
+
was the part that was easy to miss. `tr=` and `ws=` live outside the info dictionary, so BEP 9
|
|
18
|
+
never replaces them: a client keeps the magnet's copies and merges them into whatever torrent it
|
|
19
|
+
resolved to. So the client that did exactly what the magnet asked β resolved the key, landed on
|
|
20
|
+
the current build β was handed a web seed for a previous one, and every piece it fetched from
|
|
21
|
+
there failed hash verification until the peer banned it. A client that *ignored* the key and
|
|
22
|
+
joined the pinned `xt=` was fine, because there the infohash and the web seed named the same
|
|
23
|
+
build.
|
|
24
|
+
|
|
25
|
+
Nothing is lost. The metainfo carries the right web seed for whichever build was actually
|
|
26
|
+
resolved, written into `url-list` when that torrent was created, and there are two ways to reach
|
|
27
|
+
it: the `torrent=` handle added to style URLs in 0.30.0, or BEP 9 from any peer. The magnet was
|
|
28
|
+
the third route and the only one that could be wrong.
|
|
29
|
+
|
|
30
|
+
Affects the category style URL, the feed's `pmtiles:mutable`, the TileJSON `torrent.mutable
|
|
31
|
+
.magnet` and the publisher's own magnet. Archives' immutable magnets still carry their web seeds,
|
|
32
|
+
since there `xt` and `ws` cannot drift apart. The parameter was removed from `mutableMagnet`
|
|
33
|
+
outright rather than dropped at each call site, so it cannot be reintroduced by a future caller.
|
|
34
|
+
|
|
35
|
+
**Restyle anything holding one.** A style carrying an older mutable magnet keeps working, but
|
|
36
|
+
carries the stale web seed until it is regenerated.
|
|
37
|
+
|
|
38
|
+
- _...Add new stuff here..._
|
|
39
|
+
|
|
40
|
+
## 0.31.0
|
|
41
|
+
### β¨ Features and improvements
|
|
42
|
+
- **A Recheck files button, for when an archive's progress and its files disagree.** Every figure a
|
|
43
|
+
node can give you about how much of an archive is present is derived from something written down
|
|
44
|
+
earlier: the catalog's `complete` flag, the engine's resume data, the `seedOnly` claim made when
|
|
45
|
+
the torrent was added. When one of those is wrong there is no path back on its own β an archive
|
|
46
|
+
built on this node whose entry says `complete: false` is re-added without `seedOnly`, so the
|
|
47
|
+
engine goes looking for bytes that are already under its nose and sits at 0% beside a finished
|
|
48
|
+
file. Every restart reaches the same conclusion.
|
|
49
|
+
|
|
50
|
+
`POST /api/torrents/<infohash>/recheck` hashes every piece against the torrent and the result
|
|
51
|
+
wins. libtorrent does it with `force_recheck` through the sidecar (pmtiles-torrent 0.5.1 or
|
|
52
|
+
newer); qBittorrent has the same operation in its WebUI API. WebTorrent has none β it verifies on
|
|
53
|
+
add and never again β so it is re-added with the "the data is already here" claim withheld, which
|
|
54
|
+
is reported as `method: "readd"` rather than dressed up as the same mechanism.
|
|
55
|
+
|
|
56
|
+
Answers as soon as the check is under way, because hashing a planet build is tens of minutes and
|
|
57
|
+
no request should be held open for it. The archive reports state `checking` with progress as the
|
|
58
|
+
fraction hashed; progress running backwards during that is the operation working. Nothing is
|
|
59
|
+
deleted, and nothing is written to the catalog β the answer arrives where progress always does,
|
|
60
|
+
and the completion sweep already acts on it.
|
|
61
|
+
|
|
62
|
+
With two engines both are asked, since each keeps its own belief about the same file and a stale
|
|
63
|
+
one on the secondary is why a browser peer would find nothing while the primary seeds happily.
|
|
64
|
+
|
|
65
|
+
- _...Add new stuff here..._
|
|
66
|
+
|
|
67
|
+
### π Bug fixes
|
|
68
|
+
- _...Add new stuff here..._
|
|
69
|
+
|
|
10
70
|
## 0.30.0
|
|
11
71
|
### β¨ Features and improvements
|
|
12
72
|
- **The style URL now carries the `.torrent` URL as well as the magnet.** Piece hashes reach a
|
package/README.md
CHANGED
|
@@ -753,6 +753,7 @@ which the endpoint answers 501.
|
|
|
753
753
|
| `PATCH` | `/api/torrents/:infoHash/categories` | Set, add or remove categories |
|
|
754
754
|
| `PATCH` | `/api/torrents/:infoHash/seeding` | Per-archive seeding limit, or "use the global one" |
|
|
755
755
|
| `PATCH` `GET` | `/api/torrents/:infoHash/location` | Move the data; poll the move |
|
|
756
|
+
| `POST` | `/api/torrents/:infoHash/recheck` | Hash what is on disk again and believe the result β for an archive whose progress and whose files disagree |
|
|
756
757
|
| `POST` | `/api/torrents/:infoHash/pause`, `/resume` | Stop offering it, without forgetting it |
|
|
757
758
|
| `POST` | `/api/torrents/:infoHash/webseeds` | Add web seeds β does not change the infohash |
|
|
758
759
|
| `POST` | `/api/torrents/:infoHash/warm` | Pre-fetch a region (`GET` for progress, `DELETE` to cancel) |
|
package/docs/engines.md
CHANGED
|
@@ -344,6 +344,43 @@ Trackers live outside the torrent's `info` dictionary, so adding them **does not
|
|
|
344
344
|
infohash** β but it only applies to torrents created after the change. An existing archive keeps
|
|
345
345
|
announcing where its own torrent says to.
|
|
346
346
|
|
|
347
|
+
## Rechecking what is on disk
|
|
348
|
+
|
|
349
|
+
Every figure a node can give you about how much of an archive is present is derived from something
|
|
350
|
+
written down earlier: the catalog's `complete` flag, the engine's resume data, the `seedOnly` claim
|
|
351
|
+
made when the torrent was added. When one of those is wrong there is no path back on its own β the
|
|
352
|
+
archive sits at 0% beside a finished file, downloading bytes it already has, and every restart
|
|
353
|
+
repeats the same conclusion.
|
|
354
|
+
|
|
355
|
+
**Recheck files** in the console (`POST /api/torrents/<infohash>/recheck`) is the one operation
|
|
356
|
+
that goes and looks. It hashes every piece against the torrent and the result wins.
|
|
357
|
+
|
|
358
|
+
| Engine | How |
|
|
359
|
+
| ----------- | --------------------------------------------------------------- |
|
|
360
|
+
| libtorrent | `force_recheck`, via the sidecar. Needs pmtiles-torrent β₯ 0.5.1 |
|
|
361
|
+
| qBittorrent | `POST /api/v2/torrents/recheck` β the same library underneath |
|
|
362
|
+
| WebTorrent | no such operation; re-added with `seedOnly` withheld instead |
|
|
363
|
+
|
|
364
|
+
It answers as soon as the check is under way. Hashing a planet build is tens of minutes, which is
|
|
365
|
+
longer than any request should be held open for, so the archive reports state `checking` with
|
|
366
|
+
progress as the fraction hashed. **Progress running backwards during that is the operation
|
|
367
|
+
working**, not a fault. Nothing is deleted at any point.
|
|
368
|
+
|
|
369
|
+
WebTorrent's route is cruder and is reported as `method: "readd"` rather than dressed up as the
|
|
370
|
+
same thing: it verifies on add and never again, so the only way to make it look is to add the
|
|
371
|
+
torrent a second time with the "the data is already here" claim withheld. It also refuses on a
|
|
372
|
+
paused archive, since a re-add would start it.
|
|
373
|
+
|
|
374
|
+
Nothing is written to the catalog when the check begins. The answer arrives where progress always
|
|
375
|
+
does β the engine's own figures, which the completion sweep already reads. Note the one thing this
|
|
376
|
+
does not repair on its own: the sweep promotes and never demotes, so a recheck finding _less_ than
|
|
377
|
+
the record claims shows the truth in the console but leaves `complete: true` in place. Demoting on
|
|
378
|
+
a progress figure would strip the finished name off any archive that happened to be mid-check.
|
|
379
|
+
|
|
380
|
+
With two engines both are asked. Each keeps its own belief about the same file, so a stale one on
|
|
381
|
+
the secondary is why a browser peer finds nothing while the primary seeds happily. A secondary that
|
|
382
|
+
cannot check is reported and does not fail the operation; the primary failing does.
|
|
383
|
+
|
|
347
384
|
## Marking incomplete files
|
|
348
385
|
|
|
349
386
|
An archive that is not whole yet is written under a marked name and renamed when
|
|
@@ -375,9 +412,10 @@ checked and the unreadable ones are joined by magnet instead.
|
|
|
375
412
|
|
|
376
413
|
## Writing another engine
|
|
377
414
|
|
|
378
|
-
Implement `connect`, `add`, `remove`, `list`, `get`, `destroy`, and optionally `peers
|
|
379
|
-
`reachability`. Anything optional that is missing is simply not offered β an engine
|
|
380
|
-
`reachability` hides the indicator rather than reporting a node unreachable
|
|
415
|
+
Implement `connect`, `add`, `remove`, `list`, `get`, `destroy`, and optionally `peers`,
|
|
416
|
+
`reachability` and `recheck`. Anything optional that is missing is simply not offered β an engine
|
|
417
|
+
without `reachability` hides the indicator rather than reporting a node unreachable, and one
|
|
418
|
+
without `recheck` is rechecked by re-adding instead.
|
|
381
419
|
The interface is deliberately small; see
|
|
382
420
|
[`src/engines/types.js`](../src/engines/types.js) for the contract and
|
|
383
421
|
[`src/engines/webtorrent.js`](../src/engines/webtorrent.js) for the shortest example.
|
package/docs/serving-tiles.md
CHANGED
|
@@ -303,7 +303,7 @@ that publishes signs a DHT record naming whichever infohash is current, and the
|
|
|
303
303
|
magnet then names the **category** rather than a build:
|
|
304
304
|
|
|
305
305
|
```
|
|
306
|
-
magnet:?xs=urn:btpk:<public key>&s=openmaptiles&dn
|
|
306
|
+
magnet:?xs=urn:btpk:<public key>&s=openmaptiles&dn=β¦
|
|
307
307
|
```
|
|
308
308
|
|
|
309
309
|
No infohash anywhere, so nothing to go stale. A client resolves the record over
|
|
@@ -368,7 +368,7 @@ TileJSON documents from this server carry a non-standard `torrent` member:
|
|
|
368
368
|
"publicKey": "7680dc95248eb807β¦",
|
|
369
369
|
"salt": "openmaptiles",
|
|
370
370
|
"seq": 1786108931,
|
|
371
|
-
"magnet": "magnet:?xs=urn:btpk:7680dc95248eb807β¦&s=openmaptiles
|
|
371
|
+
"magnet": "magnet:?xs=urn:btpk:7680dc95248eb807β¦&s=openmaptiles"
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
374
|
}
|
package/docs/tilejson.md
CHANGED
|
@@ -77,13 +77,28 @@ serve this block β publishing is the only thing the secret is used for.
|
|
|
77
77
|
`mutable.magnet` carries **both** identifiers:
|
|
78
78
|
|
|
79
79
|
```
|
|
80
|
-
magnet:?xt=urn:btih:<current build>&xs=urn:btpk:<public key>&dn=β¦&s
|
|
80
|
+
magnet:?xt=urn:btih:<current build>&xs=urn:btpk:<public key>&dn=β¦&s=β¦
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
- `xs=urn:btpk:` is the public key. Resolving it through the DHT gives whichever
|
|
84
84
|
infohash is current, now and after every future rebuild.
|
|
85
85
|
- `xt=urn:btih:` is the build that was current when the document was generated.
|
|
86
86
|
|
|
87
|
+
**No `ws=`, and the omission is deliberate.** A web seed URL names one build;
|
|
88
|
+
this magnet names a series. They disagree the moment the next build is
|
|
89
|
+
published, and not harmlessly: `tr=` and `ws=` sit outside the info dictionary,
|
|
90
|
+
so BEP 9 never replaces them β a client keeps the magnet's copies and merges
|
|
91
|
+
them into whatever torrent it resolved to. A web seed attached to a build it
|
|
92
|
+
does not describe fails hash verification on every piece it serves, until the
|
|
93
|
+
peer banning it stops the waste.
|
|
94
|
+
|
|
95
|
+
Nothing is lost by leaving it out. `webseeds` above is the same list for the
|
|
96
|
+
build this document describes, and the metainfo for whichever build a client
|
|
97
|
+
resolves to carries its own β written into `url-list` when that torrent was
|
|
98
|
+
created, and reachable either from the `torrent=` handle in a style fragment or
|
|
99
|
+
over BEP 9 from any peer. The archive's own immutable magnet still carries web
|
|
100
|
+
seeds, because there `xt` and `ws` name the same build and cannot drift apart.
|
|
101
|
+
|
|
87
102
|
A client reads whichever it understands. A DHT-capable one resolves the key and
|
|
88
103
|
follows the series indefinitely; one without a DHT joins the infohash and gets a
|
|
89
104
|
working archive immediately.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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
|
@@ -123,7 +123,6 @@ function styleUrlFor(category, newest, base) {
|
|
|
123
123
|
salt: newest.mutable.salt ?? category,
|
|
124
124
|
// The category, since that is what this magnet resolves to.
|
|
125
125
|
name: newest.mutable.salt ?? category,
|
|
126
|
-
webSeeds: newest.webSeeds,
|
|
127
126
|
})
|
|
128
127
|
: newest?.magnet;
|
|
129
128
|
// Category-scoped rather than the newest build's immutable URL, for the same
|
|
@@ -1040,6 +1039,21 @@ export function createApp({
|
|
|
1040
1039
|
}),
|
|
1041
1040
|
);
|
|
1042
1041
|
|
|
1042
|
+
// When the record and the disk disagree, this is the only thing that goes and
|
|
1043
|
+
// looks. Answers as soon as the check is under way -- hashing a planet build
|
|
1044
|
+
// is tens of minutes, and no request should be held open for it.
|
|
1045
|
+
app.post(
|
|
1046
|
+
'/api/torrents/:infoHash/recheck',
|
|
1047
|
+
route(async (req, res) => {
|
|
1048
|
+
try {
|
|
1049
|
+
const result = await library.recheck(req.params.infoHash);
|
|
1050
|
+
res.status(202).json(result);
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
res.status(error.status ?? 500).json({ error: error.message });
|
|
1053
|
+
}
|
|
1054
|
+
}),
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1043
1057
|
// Joining defaults to cache, deliberately. This is how that is changed
|
|
1044
1058
|
// afterwards, without re-adding the archive by hand.
|
|
1045
1059
|
app.patch(
|
package/src/engines/composite.js
CHANGED
|
@@ -549,6 +549,50 @@ export class CompositeEngine {
|
|
|
549
549
|
return this.#primary.resume ? this.#primary.resume(infoHash) : false;
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Hashes what is on disk again, on every engine that can.
|
|
554
|
+
*
|
|
555
|
+
* Each engine keeps its own belief about the same file, so a stale one on the
|
|
556
|
+
* secondary is the same fault as a stale one on the primary -- it is why a
|
|
557
|
+
* browser peer would find nothing while the primary seeds happily. Both are
|
|
558
|
+
* asked.
|
|
559
|
+
*
|
|
560
|
+
* They hash concurrently, which sounds worse than it is: the second pass over
|
|
561
|
+
* a file the first just read is mostly page cache, and a recheck is a rare
|
|
562
|
+
* thing somebody asked for rather than something that runs on a timer.
|
|
563
|
+
*
|
|
564
|
+
* An engine without the operation is skipped rather than worked around. The
|
|
565
|
+
* re-add that WebTorrent needs instead is the library's to do, because it
|
|
566
|
+
* takes the catalog entry -- see Library.recheck.
|
|
567
|
+
* @param {string} infoHash - The archive to verify.
|
|
568
|
+
* @returns {Promise<object>} - The primary's answer, plus one row per engine.
|
|
569
|
+
*/
|
|
570
|
+
async recheck(infoHash) {
|
|
571
|
+
const engines = [];
|
|
572
|
+
for (const engine of [this.#primary, ...this.#secondaries]) {
|
|
573
|
+
if (!engine.recheck) {
|
|
574
|
+
engines.push({ engine: engine.name, rechecking: false, skipped: true });
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
const result = await engine.recheck(infoHash);
|
|
579
|
+
engines.push({ engine: engine.name, ...result });
|
|
580
|
+
} catch (error) {
|
|
581
|
+
// A secondary that cannot verify must not fail the primary's check.
|
|
582
|
+
// The primary holds the data the tiles are served from.
|
|
583
|
+
engines.push({
|
|
584
|
+
engine: engine.name,
|
|
585
|
+
rechecking: false,
|
|
586
|
+
error: error.message,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const primary = engines[0];
|
|
592
|
+
if (primary.error) throw new Error(primary.error);
|
|
593
|
+
return { ...primary, engines };
|
|
594
|
+
}
|
|
595
|
+
|
|
552
596
|
/**
|
|
553
597
|
* Tells every engine about a web seed.
|
|
554
598
|
* @param {string} infoHash - The archive.
|
|
@@ -303,6 +303,33 @@ export class LibtorrentEngine {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Hashes what is on disk again and believes the result over the record.
|
|
308
|
+
*
|
|
309
|
+
* Returns as soon as the check is under way, not when it finishes: a planet
|
|
310
|
+
* archive is tens of minutes of disk. The torrent reports state `checking`
|
|
311
|
+
* while it runs, with progress as the fraction hashed.
|
|
312
|
+
* @param {string} infoHash - The archive to verify.
|
|
313
|
+
* @returns {Promise<object>} - `{rechecking, wasPaused}`.
|
|
314
|
+
*/
|
|
315
|
+
async recheck(infoHash) {
|
|
316
|
+
try {
|
|
317
|
+
return await this.#call('recheck', { infoHash });
|
|
318
|
+
} catch (error) {
|
|
319
|
+
// An older sidecar answers "unknown op", which is true and useless: it
|
|
320
|
+
// reads as a bug in the request rather than as a package that needs
|
|
321
|
+
// updating. Said plainly instead, because this is a button somebody just
|
|
322
|
+
// pressed and the next thing they do depends on which it is.
|
|
323
|
+
if (/unknown op/i.test(error.message)) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
'this sidecar cannot recheck; pmtiles-torrent 0.5.1 or newer is needed',
|
|
326
|
+
{ cause: error },
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
306
333
|
async list() {
|
|
307
334
|
// A node that is shutting down still has a console polling it and a sweep
|
|
308
335
|
// or two in flight. Answering "the sidecar exited" to each of them fills
|
|
@@ -211,6 +211,20 @@ export class QBittorrentEngine {
|
|
|
211
211
|
await this.#request('/api/v2/torrents/delete', { method: 'POST', body });
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Hashes what is on disk again and believes the result over the record.
|
|
216
|
+
*
|
|
217
|
+
* qBittorrent starts the check and answers immediately, the same as the
|
|
218
|
+
* libtorrent engine does -- which is the same library underneath.
|
|
219
|
+
* @param {string} infoHash - The archive to verify.
|
|
220
|
+
* @returns {Promise<object>} - `{rechecking: true}`.
|
|
221
|
+
*/
|
|
222
|
+
async recheck(infoHash) {
|
|
223
|
+
const body = new URLSearchParams({ hashes: infoHash.toLowerCase() });
|
|
224
|
+
await this.#request('/api/v2/torrents/recheck', { method: 'POST', body });
|
|
225
|
+
return { rechecking: true };
|
|
226
|
+
}
|
|
227
|
+
|
|
214
228
|
/**
|
|
215
229
|
* Lists every torrent qBittorrent holds.
|
|
216
230
|
* @returns {Promise<import('./types.js').TorrentStatus[]>} - Normalised statuses.
|
package/src/engines/types.js
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
* @property {(filePath: string, options?: object) => Promise<object>} [createTorrent] - Builds a torrent from a local file, where the engine can do it better than the default β libtorrent produces hybrid v1+v2, which create-torrent cannot.
|
|
61
61
|
* @property {(infoHash: string) => Promise<object[]>} [trackerStatus] - Per-tracker announce results, where the engine keeps them.
|
|
62
62
|
* @property {(infoHash: string) => Promise<Uint8Array | null>} [metadata] - The torrent's metainfo once known, so an archive joined by magnet can be written down rather than re-fetched over BEP 9 on every start.
|
|
63
|
+
* @property {(infoHash: string) => Promise<object>} [recheck] - Hash what is on disk again and believe the result over the stored state -- resume data, or the `seedOnly` claim made when the torrent was added. Returns once the check is under way, not when it finishes: a planet archive is tens of minutes of disk. An engine without this is rechecked by re-adding it instead, which the library does.
|
|
63
64
|
* @property {() => Promise<object | null>} [reachability] - Whether peers can open a connection to this node, or only the reverse. Three states: `open` (something has connected inward), `unproven` (listening, but nothing ever has) and `offline` (not listening at all). The middle is deliberately not called firewalled -- on a node with no peers, blocked and untried are the same observation.
|
|
64
65
|
* @property {() => Promise<void>} destroy - Releases resources.
|
|
65
66
|
*/
|
package/src/feed.js
CHANGED
package/src/library.js
CHANGED
|
@@ -1657,6 +1657,68 @@ export class Library {
|
|
|
1657
1657
|
return this.#catalog.put({ infoHash, paused: true });
|
|
1658
1658
|
}
|
|
1659
1659
|
|
|
1660
|
+
/**
|
|
1661
|
+
* Hashes what is on disk again and believes the result over the record.
|
|
1662
|
+
*
|
|
1663
|
+
* Every other answer about how much of an archive is here comes from
|
|
1664
|
+
* something written down earlier -- the catalog's `complete` flag, resume
|
|
1665
|
+
* data, the `seedOnly` claim made when it was added. When one of those is
|
|
1666
|
+
* wrong there is no path back on its own: an archive built here whose entry
|
|
1667
|
+
* says `complete: false` is re-added without `seedOnly`, so the engine goes
|
|
1668
|
+
* looking for bytes that are already under its nose and sits at 0% beside a
|
|
1669
|
+
* finished file. This is the way out.
|
|
1670
|
+
*
|
|
1671
|
+
* Nothing is written to the catalog here. The check runs for as long as it
|
|
1672
|
+
* takes to hash the archive -- tens of minutes for a planet build -- so the
|
|
1673
|
+
* answer arrives long after this returns, and it arrives where it always
|
|
1674
|
+
* does: the engine's own progress, which the completion sweep already reads
|
|
1675
|
+
* and acts on. Recording a guess now would only have to be corrected later.
|
|
1676
|
+
*
|
|
1677
|
+
* Note what this cannot fix on its own. The sweep promotes, it never demotes,
|
|
1678
|
+
* so a recheck that finds *less* than the record claims shows the truth in
|
|
1679
|
+
* the console but leaves `complete: true` in place. That is deliberate: a
|
|
1680
|
+
* torrent reports progress below 1 for perfectly ordinary reasons while it is
|
|
1681
|
+
* checking, and demoting on that would strip the finished name off an archive
|
|
1682
|
+
* that is merely being verified.
|
|
1683
|
+
* @param {string} infoHash - The archive to verify.
|
|
1684
|
+
* @returns {Promise<object>} - `{rechecking, method}`.
|
|
1685
|
+
*/
|
|
1686
|
+
async recheck(infoHash) {
|
|
1687
|
+
const entry = this.#catalog.get(infoHash);
|
|
1688
|
+
if (!entry) {
|
|
1689
|
+
const error = new Error('unknown archive');
|
|
1690
|
+
error.status = 404;
|
|
1691
|
+
throw error;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (this.#engine.recheck) {
|
|
1695
|
+
const result = await this.#engine.recheck(infoHash);
|
|
1696
|
+
return { ...result, rechecking: true, method: 'recheck' };
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// WebTorrent has no recheck at all: it verifies on add and never again. So
|
|
1700
|
+
// the only way to make it look is to make it add the torrent again, with
|
|
1701
|
+
// the claim that the data is already there withheld -- `seedOnly` is
|
|
1702
|
+
// precisely "do not verify this", and it is computed from `complete`, so a
|
|
1703
|
+
// copy of the entry saying otherwise is what turns the check on.
|
|
1704
|
+
//
|
|
1705
|
+
// Nothing is deleted and the catalog is not touched; this re-adds the same
|
|
1706
|
+
// torrent against the same files. It is a slower and cruder mechanism than
|
|
1707
|
+
// force_recheck, and it is reported as a different one rather than dressed
|
|
1708
|
+
// up as the same thing.
|
|
1709
|
+
if (entry.paused) {
|
|
1710
|
+
const error = new Error(
|
|
1711
|
+
'this engine rechecks by re-adding the archive, which a paused ' +
|
|
1712
|
+
'archive cannot do. Resume it first.',
|
|
1713
|
+
);
|
|
1714
|
+
error.status = 409;
|
|
1715
|
+
throw error;
|
|
1716
|
+
}
|
|
1717
|
+
await this.#engine.remove(infoHash, { deleteData: false }).catch(() => {});
|
|
1718
|
+
await this.#readd({ ...entry, complete: false });
|
|
1719
|
+
return { rechecking: true, method: 'readd' };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1660
1722
|
/**
|
|
1661
1723
|
* Starts offering a paused archive again.
|
|
1662
1724
|
* @param {string} infoHash - The archive.
|
package/src/mutable.js
CHANGED
|
@@ -86,7 +86,6 @@ function rawPublicKey(publicKey) {
|
|
|
86
86
|
* @param {string} [options.infoHash] - The build that is current, so a client with no DHT has somewhere to start.
|
|
87
87
|
* @param {string} [options.name] - Display name for the archive.
|
|
88
88
|
* @param {string[]} [options.trackers] - Tracker announce URLs.
|
|
89
|
-
* @param {string[]} [options.webSeeds] - BEP 19 web seeds.
|
|
90
89
|
* @param {string} [options.salt] - Salt, when one key publishes several archives.
|
|
91
90
|
* @returns {string} - A BEP 46 magnet URI.
|
|
92
91
|
*/
|
|
@@ -129,12 +128,25 @@ export function mutableMagnet(publicKey, options = {}) {
|
|
|
129
128
|
for (const tracker of options.trackers ?? []) {
|
|
130
129
|
parts.push(`tr=${encodeURIComponent(tracker)}`);
|
|
131
130
|
}
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
// No `ws=`, and the omission is the point.
|
|
132
|
+
//
|
|
133
|
+
// A web seed URL names one build. This magnet names a series, and a client
|
|
134
|
+
// that resolves the key lands on whatever build is current -- so the two
|
|
135
|
+
// disagree the moment the next one is published. They do not disagree
|
|
136
|
+
// harmlessly: `tr=` and `ws=` live outside the info dictionary, so BEP 9
|
|
137
|
+
// never replaces them, and a client keeps the magnet's copies and merges
|
|
138
|
+
// them into whatever torrent it ends up with. The result is a web seed
|
|
139
|
+
// attached to a build it does not describe, failing hash verification on
|
|
140
|
+
// every piece it serves until the peer bans it.
|
|
141
|
+
//
|
|
142
|
+
// Nothing is lost. The metainfo carries the right web seed for whichever
|
|
143
|
+
// build the client actually resolved -- it is written into `url-list` when
|
|
144
|
+
// that build's torrent is created -- and there are two ways to reach it: the
|
|
145
|
+
// `torrent=` handle in the style fragment, or BEP 9 from any peer. This was
|
|
146
|
+
// the third route, and the only one that could be wrong.
|
|
147
|
+
//
|
|
148
|
+
// An immutable magnet is a different case and still carries its web seeds:
|
|
149
|
+
// there, `xt` and `ws` name the same build and cannot drift apart.
|
|
138
150
|
return parts.join('&');
|
|
139
151
|
}
|
|
140
152
|
|
package/src/publisher.js
CHANGED
package/src/tilejson.js
CHANGED
|
@@ -113,7 +113,6 @@ function buildTorrentBlock(entry, root) {
|
|
|
113
113
|
// The category, not this build: the record resolves to whichever
|
|
114
114
|
// archive is current, and `dn` is a label the metadata replaces.
|
|
115
115
|
name: entry.mutable.salt,
|
|
116
|
-
webSeeds: entry.webSeeds,
|
|
117
116
|
}),
|
|
118
117
|
};
|
|
119
118
|
}
|
package/src/web/index.html
CHANGED
|
@@ -1641,6 +1641,7 @@
|
|
|
1641
1641
|
<button id="set-location">Set locationβ¦</button>
|
|
1642
1642
|
<button id="add-seed">Add web seed</button>
|
|
1643
1643
|
<button id="clear-cache" ${mode === 'cache' ? '' : 'disabled title="only cache-mode archives have a cache to clear"'}>Clear cache</button>
|
|
1644
|
+
<button id="recheck" title="Hash the files on disk again and believe the result. For an archive that reads 0% next to a file that is plainly there, or one that claims to be complete and is not β every other figure comes from something written down earlier, and this is the only thing that goes and looks.">Recheck files</button>
|
|
1644
1645
|
<button id="pause">${entry.paused ? 'Resume' : 'Pause'}</button>
|
|
1645
1646
|
<button id="remove" class="danger">Remove</button>
|
|
1646
1647
|
</div>
|
|
@@ -1949,6 +1950,38 @@
|
|
|
1949
1950
|
}
|
|
1950
1951
|
};
|
|
1951
1952
|
|
|
1953
|
+
$('recheck').onclick = async () => {
|
|
1954
|
+
if (
|
|
1955
|
+
!window.confirm(
|
|
1956
|
+
`Recheck ${entry.name}.
|
|
1957
|
+
|
|
1958
|
+
Every piece is hashed against the ` +
|
|
1959
|
+
'torrent, which for a large archive is minutes to tens of ' +
|
|
1960
|
+
'minutes of disk. Nothing is deleted, and the archive keeps ' +
|
|
1961
|
+
'serving whatever it can while the check runs.',
|
|
1962
|
+
)
|
|
1963
|
+
) {
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
const result = await api(`/api/torrents/${infoHash}/recheck`, {
|
|
1968
|
+
method: 'POST',
|
|
1969
|
+
});
|
|
1970
|
+
// Said rather than left to be inferred: this returns as soon as the
|
|
1971
|
+
// check is under way, and the progress bar going backwards for the
|
|
1972
|
+
// next twenty minutes is the operation working, not a fault.
|
|
1973
|
+
toast(
|
|
1974
|
+
result.method === 'readd'
|
|
1975
|
+
? 'rechecking β this engine verifies by re-adding, so it will start from 0%'
|
|
1976
|
+
: 'rechecking β progress shows the fraction hashed',
|
|
1977
|
+
);
|
|
1978
|
+
refresh();
|
|
1979
|
+
renderDetail(infoHash);
|
|
1980
|
+
} catch (error) {
|
|
1981
|
+
toast(error.message);
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1952
1985
|
$('pause').onclick = async () => {
|
|
1953
1986
|
const action = entry.paused ? 'resume' : 'pause';
|
|
1954
1987
|
try {
|