pmtiles-swarm 0.50.0 → 0.52.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 +51 -0
- package/docs/configuration.md +65 -9
- package/package.json +1 -1
- package/src/api.js +36 -12
- package/src/catalog.js +106 -0
- package/src/config.js +20 -0
- package/src/library.js +21 -5
- package/src/web/index.html +51 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,57 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.52.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **`publishingUrl`, for the URLs that have to be permanent.** Almost every URL this node emits is
|
|
13
|
+
worked out per request, deliberately: a node answering on several domains should name itself as
|
|
14
|
+
whichever one was asked for, and leaving `publicUrl` unset is what allows that. A web seed is not
|
|
15
|
+
read once, though — it is written into the `.torrent` and the magnet, served byte for byte to
|
|
16
|
+
everyone who fetches either, and never rewritten — so it has to be one address rather than
|
|
17
|
+
whichever the last request happened to arrive on.
|
|
18
|
+
|
|
19
|
+
`publishingUrl` is that address, and nothing else. TileJSON, tile templates, `.torrent` links,
|
|
20
|
+
style URLs and the feeds go on naming whichever host the request arrived as, so the multi-domain
|
|
21
|
+
behaviour is intact. It is taken exactly as written, port and path included: a node listening on
|
|
22
|
+
8090 behind a load balancer answering on 443 sets `https://swarm.example.org` and gets it, with
|
|
23
|
+
none of the port substitution that applies to a request reaching the node directly.
|
|
24
|
+
|
|
25
|
+
- **A "Published as" field beside the web seed switch**, prefilled with whatever the node would use
|
|
26
|
+
and editable until the moment the switch is turned on. Which is the point: it is the one URL here
|
|
27
|
+
that cannot be taken back, so it should be readable — and correctable — before it is.
|
|
28
|
+
|
|
29
|
+
Resolution order is deliberate: the field, then `publishingUrl`, then `publicUrl`, then the
|
|
30
|
+
request. With none of them the setting is refused rather than invented.
|
|
31
|
+
|
|
32
|
+
## 0.51.0
|
|
33
|
+
### ✨ Features and improvements
|
|
34
|
+
- **A web seed URL that no peer could fetch is caught before it is published.** Nothing rewrites a
|
|
35
|
+
web seed afterwards — a `.torrent` is served byte for byte as it was written, so the URL that goes
|
|
36
|
+
in is the URL every peer receives for as long as the torrent exists. A loopback address is now
|
|
37
|
+
refused outright, since `127.0.0.1` names the machine asking and every peer given it would try to
|
|
38
|
+
fetch the archive from itself and retry for ever. A private address, or a hostname with no domain
|
|
39
|
+
in it, is published with a warning rather than blocked: a node syncing to its own peers across a
|
|
40
|
+
LAN is a real arrangement, and the internal address is the right answer there.
|
|
41
|
+
|
|
42
|
+
### 🐞 Bug fixes
|
|
43
|
+
- **The HTTP sources tab showed the admin port beside the serve switch.** The console built the URL
|
|
44
|
+
from the address in the browser's bar, and the console is served from the admin listener — so the
|
|
45
|
+
one port that is not for the public was the one displayed next to a switch that publishes a URL to
|
|
46
|
+
the whole swarm. It now asks the node, which already answers this correctly everywhere else. What
|
|
47
|
+
was actually written into the `.torrent` and the magnet was right throughout; only the label was
|
|
48
|
+
wrong.
|
|
49
|
+
|
|
50
|
+
## 0.50.1
|
|
51
|
+
### ✨ Features and improvements
|
|
52
|
+
|
|
53
|
+
### 🐞 Bug fixes
|
|
54
|
+
- **The HTTP sources tab threw instead of opening.** The three switches were built with the base URL
|
|
55
|
+
that `renderDetail` keeps, and they are rendered by `fillPane`, which is a different function with
|
|
56
|
+
no sight of it — a `ReferenceError` the moment the tab was clicked. The console is one module
|
|
57
|
+
inside an HTML file, so there is nothing to import and nothing to lint: `node --check` accepts a
|
|
58
|
+
binding reached across a function boundary and says nothing. A test now compares what `fillPane`
|
|
59
|
+
uses against what only `renderDetail` declares.
|
|
60
|
+
|
|
10
61
|
## 0.50.0
|
|
11
62
|
### ✨ Features and improvements
|
|
12
63
|
- **Three switches for what a node offers of an archive's own bytes**, on the node, on a watched
|
package/docs/configuration.md
CHANGED
|
@@ -315,11 +315,12 @@ rotation stays out across the restart you were probably about to do.
|
|
|
315
315
|
|
|
316
316
|
## Offering the archive file itself
|
|
317
317
|
|
|
318
|
-
| setting | default |
|
|
319
|
-
| ---------------- | ------- |
|
|
320
|
-
| `serveArchive` | `false` | answer `/archives/<infohash>/archive.pmtiles`
|
|
321
|
-
| `
|
|
322
|
-
| `
|
|
318
|
+
| setting | default | |
|
|
319
|
+
| ---------------- | ------- | ------------------------------------------------------- |
|
|
320
|
+
| `serveArchive` | `false` | answer `/archives/<infohash>/archive.pmtiles` |
|
|
321
|
+
| `publishingUrl` | unset | the address to use where a URL must outlive the request |
|
|
322
|
+
| `selfWebSeed` | `false` | publish this node as a web seed for archives it holds |
|
|
323
|
+
| `publicDownload` | `false` | offer them as downloads on the public catalogue page |
|
|
323
324
|
|
|
324
325
|
Three switches rather than one, because they are three different exposures and a
|
|
325
326
|
node can reasonably want any of them without the others.
|
|
@@ -345,6 +346,36 @@ GET /latest/<category>/archive.pmtiles whichever is current, with an ETag
|
|
|
345
346
|
|
|
346
347
|
With it off, both answer `403`.
|
|
347
348
|
|
|
349
|
+
### `publishingUrl`
|
|
350
|
+
|
|
351
|
+
The address to use for URLs that outlive the request that made them.
|
|
352
|
+
|
|
353
|
+
Almost every URL this node emits is worked out per request, deliberately: a node
|
|
354
|
+
answering on several domains should name itself as whichever one was asked for,
|
|
355
|
+
and leaving [`publicUrl`](#publicurl) unset is what allows that. Read once, by
|
|
356
|
+
whoever asked, a per-request answer is the correct answer.
|
|
357
|
+
|
|
358
|
+
A web seed is not read once. It is written into the `.torrent` and the magnet,
|
|
359
|
+
served byte for byte to everyone who fetches either, and never rewritten — so it
|
|
360
|
+
has to be one address rather than whichever the last request happened to arrive
|
|
361
|
+
on. This is that address.
|
|
362
|
+
|
|
363
|
+
Narrower than `publicUrl` on purpose: `publicUrl` overrides every URL the node
|
|
364
|
+
emits and so gives up the multi-domain behaviour, while this overrides only the
|
|
365
|
+
ones that have to be permanent. Everything else — TileJSON, tile templates,
|
|
366
|
+
`.torrent` links, style URLs, the feeds — goes on naming whichever host the
|
|
367
|
+
request arrived as.
|
|
368
|
+
|
|
369
|
+
```json
|
|
370
|
+
{
|
|
371
|
+
"publishingUrl": "https://swarm.example.org"
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Unset falls back to `publicUrl` and then to the request. Whatever it resolves to
|
|
376
|
+
is what the console shows in the **Published as** field, where it can be
|
|
377
|
+
corrected before it becomes permanent.
|
|
378
|
+
|
|
348
379
|
### `selfWebSeed`
|
|
349
380
|
|
|
350
381
|
Writes this node's own `archive.pmtiles` URL into the torrent's `url-list`, so
|
|
@@ -358,10 +389,35 @@ node. Turning it off takes the URL back out of the `.torrent` and the magnet —
|
|
|
358
389
|
but peers already holding either keep trying it until they refresh, so this
|
|
359
390
|
withdraws an advertisement rather than closing a door.
|
|
360
391
|
|
|
361
|
-
The node has to know what it is called
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
392
|
+
The node has to know what it is called, and this is the one URL where "whichever
|
|
393
|
+
name was asked for" is not good enough. In order of how deliberate it is:
|
|
394
|
+
|
|
395
|
+
1. **The field beside the switch in the console.** Prefilled with whatever the
|
|
396
|
+
node would use, editable until the moment the switch is turned on, and after
|
|
397
|
+
that it is what peers hold.
|
|
398
|
+
2. **[`publishingUrl`](#publishingurl)** — the node's answer for exactly this
|
|
399
|
+
question, and the one to set on a node that answers on several domains.
|
|
400
|
+
3. **[`publicUrl`](#publicurl)**, for a node that has overridden everything
|
|
401
|
+
anyway.
|
|
402
|
+
4. **The request**, which is a guess, but the same guess every other URL makes.
|
|
403
|
+
|
|
404
|
+
With none of them the setting is refused rather than invented: a guessed web
|
|
405
|
+
seed URL is published and then followed.
|
|
406
|
+
|
|
407
|
+
**Nothing rewrites a web seed after it is published.** A `.torrent` is served
|
|
408
|
+
byte for byte as it was written, so the URL that went in is the URL every peer
|
|
409
|
+
receives, for as long as the torrent exists. Two consequences:
|
|
410
|
+
|
|
411
|
+
- A loopback address is refused. `127.0.0.1` names the machine asking, so every
|
|
412
|
+
peer given it would try to fetch the archive from itself and retry for ever.
|
|
413
|
+
- A private address, or a hostname with no domain in it, is published with a
|
|
414
|
+
warning rather than blocked. A node syncing to its own peers across a LAN is a
|
|
415
|
+
real arrangement and the internal address is the right answer there — but it is
|
|
416
|
+
worth knowing that peers outside that network cannot use it.
|
|
417
|
+
|
|
418
|
+
The console asks the node for the URL rather than building one from the address
|
|
419
|
+
in the browser's bar, since the console is served from the admin listener and
|
|
420
|
+
that is the one port that is not for the public.
|
|
365
421
|
|
|
366
422
|
### `publicDownload`
|
|
367
423
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.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
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
hashToken,
|
|
14
14
|
isPublicSurface,
|
|
15
15
|
} from './auth.js';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
normalizeCategories,
|
|
18
|
+
publishingBase,
|
|
19
|
+
publishingFor,
|
|
20
|
+
} from './catalog.js';
|
|
17
21
|
import { mutableMagnet, trackersFromMagnet } from './mutable.js';
|
|
18
22
|
import { guessKind } from './library.js';
|
|
19
23
|
import { QBittorrentEngine } from './engines/qbittorrent.js';
|
|
@@ -1014,7 +1018,26 @@ export function createApp({
|
|
|
1014
1018
|
reading,
|
|
1015
1019
|
diskBytes,
|
|
1016
1020
|
served,
|
|
1017
|
-
publishing:
|
|
1021
|
+
publishing: {
|
|
1022
|
+
...publishingFor(entry, config),
|
|
1023
|
+
// What this node would actually publish, worked out here rather than
|
|
1024
|
+
// in the browser. The console is served from the admin listener, so
|
|
1025
|
+
// `location.origin` there names the one port that is not for the
|
|
1026
|
+
// public — and a web seed built from it is handed to every peer in
|
|
1027
|
+
// the swarm.
|
|
1028
|
+
// Whatever was actually published for this archive, where one was:
|
|
1029
|
+
// the record is the truth about what peers hold, and it need not be
|
|
1030
|
+
// what this node would build today.
|
|
1031
|
+
url:
|
|
1032
|
+
entry.selfWebSeedUrl ??
|
|
1033
|
+
`${publishingBase({
|
|
1034
|
+
config,
|
|
1035
|
+
requestBase: baseUrl(req),
|
|
1036
|
+
})}/archives/${entry.infoHash}/archive.pmtiles`,
|
|
1037
|
+
// The base the switch would use, so the console can offer it for
|
|
1038
|
+
// editing before anything permanent is written.
|
|
1039
|
+
base: publishingBase({ config, requestBase: baseUrl(req) }),
|
|
1040
|
+
},
|
|
1018
1041
|
});
|
|
1019
1042
|
}),
|
|
1020
1043
|
);
|
|
@@ -1137,16 +1160,17 @@ export function createApp({
|
|
|
1137
1160
|
route(async (req, res) => {
|
|
1138
1161
|
const body = req.body ?? {};
|
|
1139
1162
|
try {
|
|
1140
|
-
const result = await library.setPublishing(
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
//
|
|
1144
|
-
|
|
1145
|
-
//
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
|
|
1149
|
-
|
|
1163
|
+
const result = await library.setPublishing(req.params.infoHash, body, {
|
|
1164
|
+
// Typed into the field beside the switch, where it beats
|
|
1165
|
+
// everything else: this is the one URL a person gets to decide
|
|
1166
|
+
// deliberately, because it is the one that cannot be taken back.
|
|
1167
|
+
publishingUrl: body.publishingUrl,
|
|
1168
|
+
// Otherwise the request it is being asked on is the best evidence
|
|
1169
|
+
// available, the same reasoning the TileJSON already uses — and
|
|
1170
|
+
// with the public port rather than the admin one, since a seed URL
|
|
1171
|
+
// on a listener bound to localhost reaches no peer at all.
|
|
1172
|
+
baseUrl: baseUrl(req),
|
|
1173
|
+
});
|
|
1150
1174
|
res.json(result);
|
|
1151
1175
|
} catch (error) {
|
|
1152
1176
|
const status = /unknown archive/.test(error.message) ? 404 : 400;
|
package/src/catalog.js
CHANGED
|
@@ -260,3 +260,109 @@ export class Catalog {
|
|
|
260
260
|
return this.#writing;
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Whether a URL is one other people could plausibly fetch.
|
|
265
|
+
*
|
|
266
|
+
* A web seed is not a setting; it is written into the `.torrent` and the
|
|
267
|
+
* magnet, and served to everyone who asks for either. Nothing rewrites it
|
|
268
|
+
* afterwards — the file is handed out byte for byte — so a URL that names
|
|
269
|
+
* this machine's own loopback interface is not a mistake that gets corrected
|
|
270
|
+
* later. It is distributed, followed, and retried by every peer in the swarm
|
|
271
|
+
* for as long as the torrent exists.
|
|
272
|
+
*
|
|
273
|
+
* Loopback is the only case refused outright, because it cannot be right for
|
|
274
|
+
* anybody: `127.0.0.1` means the peer's own machine, not this one. A private
|
|
275
|
+
* address is a different matter — a node syncing to its own peers across a LAN
|
|
276
|
+
* is a real arrangement, and this is not the place to overrule it — so that is
|
|
277
|
+
* reported rather than blocked.
|
|
278
|
+
* @param {string} url - The candidate web seed.
|
|
279
|
+
* @returns {{ok: boolean, why?: string, warning?: string}} - Whether to
|
|
280
|
+
* publish it, and what to say about it either way.
|
|
281
|
+
*/
|
|
282
|
+
export function reachability(url) {
|
|
283
|
+
let host;
|
|
284
|
+
try {
|
|
285
|
+
host = new URL(url).hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
286
|
+
} catch {
|
|
287
|
+
return { ok: false, why: `not a URL: ${url}` };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (host === 'localhost' || host === '::1' || /^127\./.test(host)) {
|
|
291
|
+
return {
|
|
292
|
+
ok: false,
|
|
293
|
+
why:
|
|
294
|
+
`${host} names the machine asking, not this one, so a web seed at ` +
|
|
295
|
+
'this address is unusable by every peer that receives it. Set ' +
|
|
296
|
+
'publicUrl to the address this node is reachable at.',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const private4 =
|
|
301
|
+
/^10\./.test(host) ||
|
|
302
|
+
/^192\.168\./.test(host) ||
|
|
303
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(host) ||
|
|
304
|
+
/^169\.254\./.test(host);
|
|
305
|
+
// fc00::/7, the IPv6 unique-local range.
|
|
306
|
+
const private6 = /^f[cd][0-9a-f]{2}:/.test(host);
|
|
307
|
+
if (private4 || private6 || host.endsWith('.local')) {
|
|
308
|
+
return {
|
|
309
|
+
ok: true,
|
|
310
|
+
warning:
|
|
311
|
+
`${host} is a private address. It will be published in the .torrent ` +
|
|
312
|
+
'and the magnet as it stands, and nothing rewrites it later — peers ' +
|
|
313
|
+
'outside this network cannot use it.',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// A bare name with no dot in it resolves only where the same search domain
|
|
318
|
+
// does, which is a narrower place than the swarm.
|
|
319
|
+
if (!host.includes('.')) {
|
|
320
|
+
return {
|
|
321
|
+
ok: true,
|
|
322
|
+
warning:
|
|
323
|
+
`${host} has no domain, so it resolves only on networks that already ` +
|
|
324
|
+
'know the name. Peers elsewhere cannot use it.',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { ok: true };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The base a URL gets when it is going to outlive the request that made it.
|
|
333
|
+
*
|
|
334
|
+
* Almost every URL this node emits is worked out per request, on purpose: a
|
|
335
|
+
* node answering on several domains should name itself as whichever one was
|
|
336
|
+
* asked, and `publicUrl` is deliberately left unset to allow that. That is the
|
|
337
|
+
* right answer for a TileJSON, a `.torrent` link or a style URL — read once,
|
|
338
|
+
* by whoever asked, and correct for them.
|
|
339
|
+
*
|
|
340
|
+
* A web seed is not that. It is written into the `.torrent` and the magnet and
|
|
341
|
+
* served byte for byte to everyone who asks for either, so it has to be one
|
|
342
|
+
* address rather than whichever the last request happened to arrive on. This
|
|
343
|
+
* is where that address comes from, in order of how deliberate it is:
|
|
344
|
+
*
|
|
345
|
+
* 1. Given outright — the field beside the switch in the console.
|
|
346
|
+
* 2. `publishingUrl`, the node's answer for exactly this question.
|
|
347
|
+
* 3. `publicUrl`, if a node has overridden everything anyway.
|
|
348
|
+
* 4. The request, which is a guess, but the same guess every other URL makes.
|
|
349
|
+
*
|
|
350
|
+
* @param {object} [options] - `explicit`, `config` and `requestBase`.
|
|
351
|
+
* @returns {string} - A base with no trailing slash, or an empty string.
|
|
352
|
+
*/
|
|
353
|
+
export function publishingBase({ explicit, config, requestBase } = {}) {
|
|
354
|
+
const candidates = [
|
|
355
|
+
explicit,
|
|
356
|
+
config?.publishingUrl,
|
|
357
|
+
config?.publicUrl,
|
|
358
|
+
requestBase,
|
|
359
|
+
];
|
|
360
|
+
for (const candidate of candidates) {
|
|
361
|
+
// An empty or whitespace value means "not set", not "use an empty base" —
|
|
362
|
+
// the same reading `publicUrl` gets, and for the same reason: clearing a
|
|
363
|
+
// key by emptying it is what an operator naturally does to a JSON file.
|
|
364
|
+
const value = String(candidate ?? '').trim();
|
|
365
|
+
if (value) return value.replace(/\/+$/, '');
|
|
366
|
+
}
|
|
367
|
+
return '';
|
|
368
|
+
}
|
package/src/config.js
CHANGED
|
@@ -234,6 +234,26 @@ const DEFAULTS = {
|
|
|
234
234
|
allowUnauthenticated: false,
|
|
235
235
|
/** Public base URL, used to build absolute links in the RSS feed and TileJSON. */
|
|
236
236
|
publicUrl: undefined,
|
|
237
|
+
/**
|
|
238
|
+
* The one address to use for URLs that outlive the request that made them.
|
|
239
|
+
*
|
|
240
|
+
* Almost every URL this node emits is worked out per request, deliberately: a
|
|
241
|
+
* node answering on several domains should name itself as whichever one was
|
|
242
|
+
* asked for, and leaving `publicUrl` unset is what allows that. Read once, by
|
|
243
|
+
* whoever asked, a per-request answer is the correct answer.
|
|
244
|
+
*
|
|
245
|
+
* A web seed is not read once. It is written into the `.torrent` and the
|
|
246
|
+
* magnet, served byte for byte to everyone who fetches either, and never
|
|
247
|
+
* rewritten — so it has to be one address rather than whichever the last
|
|
248
|
+
* request happened to arrive on. Set this to that address.
|
|
249
|
+
*
|
|
250
|
+
* Narrower than `publicUrl` on purpose. `publicUrl` overrides every URL the
|
|
251
|
+
* node emits and so gives up the multi-domain behaviour; this overrides only
|
|
252
|
+
* the ones that have to be permanent. Unset falls back to `publicUrl`, then
|
|
253
|
+
* to the request — and the console lets the address be typed at the moment
|
|
254
|
+
* the switch is turned on, which beats both.
|
|
255
|
+
*/
|
|
256
|
+
publishingUrl: undefined,
|
|
237
257
|
/**
|
|
238
258
|
* Trust X-Forwarded-* headers, for running behind a reverse proxy or CDN.
|
|
239
259
|
* Takes anything Express accepts: `true`, a hop count, or a subnet list.
|
package/src/library.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
promote,
|
|
12
12
|
suffixFor,
|
|
13
13
|
} from './incomplete.js';
|
|
14
|
-
import { publishingFor } from './catalog.js';
|
|
14
|
+
import { publishingBase, publishingFor, reachability } from './catalog.js';
|
|
15
15
|
import { checkOrigin, fingerprintOrigin } from './origin.js';
|
|
16
16
|
import { probePMTiles } from './pmtiles-probe.js';
|
|
17
17
|
import {
|
|
@@ -2869,18 +2869,31 @@ export class Library {
|
|
|
2869
2869
|
// so nothing would ever have published it.
|
|
2870
2870
|
const published = entry.selfWebSeedUrl ?? null;
|
|
2871
2871
|
let webSeed = published;
|
|
2872
|
+
let warning = null;
|
|
2872
2873
|
|
|
2873
2874
|
if (after.selfWebSeed && !published) {
|
|
2874
|
-
const base =
|
|
2875
|
-
|
|
2876
|
-
.
|
|
2875
|
+
const base = publishingBase({
|
|
2876
|
+
// Given outright beats the node's own answer, which beats the request
|
|
2877
|
+
// this happens to have arrived on. See publishingBase().
|
|
2878
|
+
explicit: options.publishingUrl,
|
|
2879
|
+
config: this.#config,
|
|
2880
|
+
requestBase: options.baseUrl,
|
|
2881
|
+
});
|
|
2877
2882
|
if (!base) {
|
|
2878
2883
|
throw new Error(
|
|
2879
2884
|
'cannot publish this node as a web seed without knowing its own ' +
|
|
2880
|
-
'URL: set
|
|
2885
|
+
'URL: set publishingUrl, or give one with the request',
|
|
2881
2886
|
);
|
|
2882
2887
|
}
|
|
2883
2888
|
webSeed = `${base}/archives/${infoHash}/archive.pmtiles`;
|
|
2889
|
+
// Checked before it goes anywhere. Nothing rewrites a web seed once it
|
|
2890
|
+
// is in a .torrent -- the file is served byte for byte to everyone who
|
|
2891
|
+
// asks -- so a URL that cannot work is not a mistake that gets corrected
|
|
2892
|
+
// on the next request. It is distributed and then retried for ever.
|
|
2893
|
+
const reach = reachability(webSeed);
|
|
2894
|
+
if (!reach.ok) throw new Error(reach.why);
|
|
2895
|
+
if (reach.warning) console.warn(`[web seed] ${reach.warning}`);
|
|
2896
|
+
warning = reach.warning ?? null;
|
|
2884
2897
|
await this.addWebSeeds(infoHash, [webSeed]);
|
|
2885
2898
|
// Only the field. put() merges, so spreading the entry captured before
|
|
2886
2899
|
// addWebSeeds would write the old webSeeds and the old magnet back over
|
|
@@ -2900,6 +2913,9 @@ export class Library {
|
|
|
2900
2913
|
return {
|
|
2901
2914
|
...after,
|
|
2902
2915
|
webSeed,
|
|
2916
|
+
// Published anyway, and said out loud. A node syncing to its own peers
|
|
2917
|
+
// across a LAN is a real arrangement and not this code's to overrule.
|
|
2918
|
+
warning,
|
|
2903
2919
|
// Named so a caller can warn about the one change that is not merely a
|
|
2904
2920
|
// setting moving: this URL is already in the hands of every peer that
|
|
2905
2921
|
// holds the torrent, and they will go on trying it for a while.
|
package/src/web/index.html
CHANGED
|
@@ -392,6 +392,13 @@
|
|
|
392
392
|
would rearrange the panel under a click, and would leave no sign of
|
|
393
393
|
why the option had gone. */
|
|
394
394
|
.publish input:disabled + span { opacity: 0.55; }
|
|
395
|
+
/* The address a web seed would be published at. Beside the switch it
|
|
396
|
+
belongs to rather than in settings, because it is decided once, at the
|
|
397
|
+
moment it stops being editable. */
|
|
398
|
+
.pubbase { margin: -0.2rem 0 0 1.6rem; }
|
|
399
|
+
.pubbase.hidden { display: none; }
|
|
400
|
+
.pubbase label { display: block; font-size: 0.8rem; color: var(--muted); }
|
|
401
|
+
.pubbase input { width: 100%; max-width: 32rem; margin: 0.2rem 0; }
|
|
395
402
|
</style>
|
|
396
403
|
</head>
|
|
397
404
|
<body>
|
|
@@ -1821,7 +1828,15 @@
|
|
|
1821
1828
|
// says nothing defers to the node's own answer, and what belongs
|
|
1822
1829
|
// in a checkbox is what is actually happening.
|
|
1823
1830
|
const on = entry.publishing ?? {};
|
|
1824
|
-
|
|
1831
|
+
// The node's own answer, not this page's. The console is served
|
|
1832
|
+
// from the admin listener, so `location.origin` here is the one
|
|
1833
|
+
// port that is not for the public — showing it beside a switch
|
|
1834
|
+
// that publishes a URL to the whole swarm is how a useless web
|
|
1835
|
+
// seed gets advertised. The fallback is for an older node that
|
|
1836
|
+
// does not send one.
|
|
1837
|
+
const archiveUrl =
|
|
1838
|
+
on.url ??
|
|
1839
|
+
`${location.origin}/archives/${entry.infoHash}/archive.pmtiles`;
|
|
1825
1840
|
|
|
1826
1841
|
// Three switches rather than one, because they are three separate
|
|
1827
1842
|
// exposures. Serving the file to a reader that was handed the URL,
|
|
@@ -1862,6 +1877,25 @@
|
|
|
1862
1877
|
while.`,
|
|
1863
1878
|
!on.serveArchive,
|
|
1864
1879
|
)}
|
|
1880
|
+
<div class="pubbase${on.serveArchive ? '' : ' hidden'}">
|
|
1881
|
+
<label for="publish-base">Published as</label>
|
|
1882
|
+
<input
|
|
1883
|
+
id="publish-base"
|
|
1884
|
+
value="${escapeHtml(on.base ?? '')}"
|
|
1885
|
+
placeholder="https://swarm.example.org"
|
|
1886
|
+
spellcheck="false"
|
|
1887
|
+
${on.selfWebSeed ? 'disabled' : ''}
|
|
1888
|
+
/>
|
|
1889
|
+
<div class="sub">
|
|
1890
|
+
The address peers will be given, and the one thing here
|
|
1891
|
+
that cannot be taken back: a <code>.torrent</code> is
|
|
1892
|
+
served exactly as it was written, so this URL reaches
|
|
1893
|
+
everyone who ever fetches it. Filled in from
|
|
1894
|
+
<code>publishingUrl</code>, or from this request when
|
|
1895
|
+
there is none — worth correcting if the node answers on
|
|
1896
|
+
more than one name.
|
|
1897
|
+
</div>
|
|
1898
|
+
</div>
|
|
1865
1899
|
${toggle(
|
|
1866
1900
|
'publicDownload',
|
|
1867
1901
|
'Offer it as a download on the public page',
|
|
@@ -1922,10 +1956,25 @@
|
|
|
1922
1956
|
}
|
|
1923
1957
|
box.disabled = true;
|
|
1924
1958
|
try {
|
|
1959
|
+
// Sent only with the switch it belongs to, so editing the
|
|
1960
|
+
// field and then toggling something else cannot rewrite a
|
|
1961
|
+
// seed nobody was looking at.
|
|
1962
|
+
const typed =
|
|
1963
|
+
key === 'selfWebSeed' && want
|
|
1964
|
+
? pane.querySelector('#publish-base')?.value.trim()
|
|
1965
|
+
: undefined;
|
|
1925
1966
|
const result = await api(
|
|
1926
1967
|
`/api/torrents/${infoHash}/publish`,
|
|
1927
|
-
{
|
|
1968
|
+
{
|
|
1969
|
+
method: 'POST',
|
|
1970
|
+
body: { [key]: want, publishingUrl: typed || undefined },
|
|
1971
|
+
},
|
|
1928
1972
|
);
|
|
1973
|
+
// A seed URL nothing will rewrite later, published to a
|
|
1974
|
+
// network smaller than the swarm. Worth interrupting for:
|
|
1975
|
+
// by the time a peer fails to fetch it, the URL is in every
|
|
1976
|
+
// copy of the torrent.
|
|
1977
|
+
if (result.warning) toast(result.warning);
|
|
1929
1978
|
// The withdrawal is worth saying out loud. The others are
|
|
1930
1979
|
// visible in the checkbox that was just clicked.
|
|
1931
1980
|
if (result.withdrewWebSeed) {
|