pmtiles-swarm 0.6.0 → 0.7.1
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 +52 -0
- package/README.md +19 -7
- package/docs/subscribing.md +19 -3
- package/package.json +1 -1
- package/src/config.js +15 -0
- package/src/retention.js +16 -0
- package/src/subscriptions.js +54 -0
- package/src/web/index.html +148 -32
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,58 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.7.1
|
|
11
|
+
### 🐞 Bug fixes
|
|
12
|
+
- **A feed no longer deletes its only complete copy.** Retention was written for a watched folder
|
|
13
|
+
and a scheduled source, where the archive it is handed is already whole — the file was there, or
|
|
14
|
+
the fetch finished. A subscription is not like that: it joins a torrent, and the data arrives
|
|
15
|
+
hours later. So `keep: 1` on a feed removed last week's complete copy the moment this week's
|
|
16
|
+
torrent was announced, leaving nothing complete at all for the length of an 88 GiB download.
|
|
17
|
+
`keepDays` had the same exposure, a copy ageing out while its replacement was still arriving.
|
|
18
|
+
|
|
19
|
+
Retention on a subscription now waits for the newest copy to be whole, which makes `keep: 1`
|
|
20
|
+
mean *the last complete copy* — the only reading of it that is safe there. It also runs on every
|
|
21
|
+
poll rather than only on polls that took something, because what is being waited for is a
|
|
22
|
+
download finishing rather than a poll happening. Watched folders and scheduled sources are
|
|
23
|
+
unchanged: they hand over a finished archive, and asking them for a completion marker they never
|
|
24
|
+
set would have stopped their retention working.
|
|
25
|
+
|
|
26
|
+
### 📚 Documentation
|
|
27
|
+
- **`prune` does not apply to an RSS subscription**, which the documentation did not say and a
|
|
28
|
+
reader would reasonably have assumed otherwise — it is accepted there and quietly does nothing.
|
|
29
|
+
Absence from a bounded feed is not evidence that anything was withdrawn, so pruning needs a
|
|
30
|
+
catalogue. The two questions are now separated where they are described: whether the publisher
|
|
31
|
+
still offers an archive, and whether you still want it on your disk.
|
|
32
|
+
- **Feed retention is documented**, along with the claim it replaces. The subscribing guide said a
|
|
33
|
+
node following a feed "accumulates and never sheds", which was true when it was written and is
|
|
34
|
+
what `keep` and `keepDays` on a subscription now answer.
|
|
35
|
+
- **The README describes the two settings sections** rather than the single table they replaced,
|
|
36
|
+
and covers `newest`, `keep` and `keepDays` — none of which it mentioned.
|
|
37
|
+
|
|
38
|
+
## 0.7.0
|
|
39
|
+
### ✨ Features and improvements
|
|
40
|
+
- **A feed can be told how long to keep what it brings in.** `keep` and `keepDays` now work on a
|
|
41
|
+
subscription, applied by the same code a watched folder and a scheduled source retire under and
|
|
42
|
+
with the same guards: only after something new has landed, and never the newest copy. A feed
|
|
43
|
+
publishing weekly leaves a copy behind every week and the publisher goes on listing all of them,
|
|
44
|
+
so `keepDays: 10` against a weekly feed keeps a fortnight and drops the rest.
|
|
45
|
+
|
|
46
|
+
This is not what `prune` does, and the difference is why a feed needed its own answer. Pruning is
|
|
47
|
+
about the publisher — it stopped offering this, so let it go — and needs a complete listing for an
|
|
48
|
+
absence to mean anything, which is why it applies to a catalogue and not to a feed.
|
|
49
|
+
planet.openstreetmap.org lists five dumps and says nothing about the hundreds before them. Age is
|
|
50
|
+
age however short the list is.
|
|
51
|
+
- **Feeds and remote nodes are two sections rather than one table with a dropdown.** They are not
|
|
52
|
+
one thing in two costumes: a feed is bounded and says "here is what is new", so it caps items per
|
|
53
|
+
check and can never prune; a catalogue says "here is everything", which is the only thing that
|
|
54
|
+
makes an absence meaningful. Half the columns applied to one and half to the other. A row is now
|
|
55
|
+
RSS or API because of the table it sits in, which also means a saved row states its protocol
|
|
56
|
+
instead of leaving it to be guessed from the URL later. Rows written before this are sorted into
|
|
57
|
+
a section by the same rule the subscription manager itself uses.
|
|
58
|
+
- **A feed's save path is editable.** `savePath` was accepted in the configuration and offered
|
|
59
|
+
nowhere, so it could only be set by hand — and before the row editor learned to keep fields it
|
|
60
|
+
does not show, pressing Save would have deleted it.
|
|
61
|
+
|
|
10
62
|
## 0.6.0
|
|
11
63
|
### ✨ Features and improvements
|
|
12
64
|
- **Check now, on scheduled sources and on feeds.** A schedule describes ordinary operation, and
|
package/README.md
CHANGED
|
@@ -488,16 +488,28 @@ restart.
|
|
|
488
488
|
|
|
489
489
|
### Following other nodes
|
|
490
490
|
|
|
491
|
-
`subscriptions` are
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
491
|
+
`subscriptions` are what this node takes archives from, editable in Settings as two sections:
|
|
492
|
+
**RSS feeds** and **Remote nodes**. They are not one thing in two costumes. A feed says "here is
|
|
493
|
+
what is new" and is bounded by the publisher's `feedMaxItems`, so a node offline long enough
|
|
494
|
+
misses things permanently, and an absence from one proves nothing; a `/api/catalog` URL says
|
|
495
|
+
"here is everything", which is what makes reconciling — and pruning — possible. A row belongs to
|
|
496
|
+
whichever section it sits in, and is saved with that protocol rather than leaving it to be
|
|
497
|
+
inferred from the URL.
|
|
495
498
|
|
|
496
499
|
`mode` decides what following one costs: `cache` joins the swarm and fetches only what is read,
|
|
497
500
|
`mirror` commits to a whole copy of every archive the peer lists. `token` is presented to the peer,
|
|
498
|
-
which may then publish more than it publishes to the world.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
+
which may then publish more than it publishes to the world.
|
|
502
|
+
|
|
503
|
+
`newest` caps how many items one check of a **feed** may take, counting from the newest, and
|
|
504
|
+
defaults to 1 — planet.openstreetmap.org lists five planet dumps, and taking the lot is four
|
|
505
|
+
hundred gigabytes nobody asked for. It means nothing to a catalogue, which lists everything.
|
|
506
|
+
|
|
507
|
+
`keep` and `keepDays` retire what a subscription has brought in, exactly as they do for a watched
|
|
508
|
+
folder or a scheduled source: only after something new has landed, and never the newest copy. They
|
|
509
|
+
answer for your disk. `prune` answers for the publisher — it is off unless chosen, only ever
|
|
510
|
+
considers archives that peer sent, never acts on a filtered or partial view, and **applies to a
|
|
511
|
+
catalogue only**, since absence from a bounded feed is not evidence that anything was withdrawn.
|
|
512
|
+
Start a new peer on `"report"` and watch it before trusting it with more.
|
|
501
513
|
|
|
502
514
|
Peer tokens are redacted from `GET /api/config` like any other credential, and a save that echoes
|
|
503
515
|
the placeholder back keeps the stored one.
|
package/docs/subscribing.md
CHANGED
|
@@ -117,9 +117,16 @@ poor trade for the archive itself.
|
|
|
117
117
|
|
|
118
118
|
## Removing what a peer no longer offers
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
Two different questions, with two different answers, and it is worth keeping
|
|
121
|
+
them apart: whether the **publisher** still offers an archive, and whether
|
|
122
|
+
**you** still want it on your disk.
|
|
123
|
+
|
|
124
|
+
### The publisher stopped offering it
|
|
125
|
+
|
|
126
|
+
A feed can only ever say "here is something new". Nothing about it distinguishes
|
|
127
|
+
"withdrawn" from "further down a list that only carries five items", so absence
|
|
128
|
+
from a feed is not evidence of anything and **`prune` does not apply to an RSS
|
|
129
|
+
subscription at all** — setting it there is quietly inert.
|
|
123
130
|
|
|
124
131
|
The catalogue API can say "here is everything", which is the only way a
|
|
125
132
|
consumer can notice an absence. `prune` acts on that, and it is deliberately
|
|
@@ -289,6 +296,15 @@ OpenStreetMap publishes for the planet dumps:
|
|
|
289
296
|
it is **1 by default**. That feed lists five planet dumps, so without a cap the
|
|
290
297
|
first poll is roughly four hundred gigabytes. `0` takes everything it lists.
|
|
291
298
|
|
|
299
|
+
`keepDays` is how long to keep what it brings in. A feed publishing weekly
|
|
300
|
+
leaves a copy behind every week and the publisher goes on listing all of them,
|
|
301
|
+
so without it a subscription grows for ever — which is the case pruning cannot
|
|
302
|
+
help with, because nothing was ever withdrawn. `keep` says the same thing as a
|
|
303
|
+
count. Both are the rules a watched folder and a scheduled source retire under,
|
|
304
|
+
applied by the same code and with the same guards: **only after something new
|
|
305
|
+
has landed**, and **never the newest copy**, so a feed that goes quiet cannot
|
|
306
|
+
empty your disk.
|
|
307
|
+
|
|
292
308
|
The items are `.osm.pbf`, not map archives, and that is fine: joining an
|
|
293
309
|
existing torrent does not require the content to be anything in particular —
|
|
294
310
|
only *creating* one does. The archive simply is not servable as tiles, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
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/config.js
CHANGED
|
@@ -555,6 +555,21 @@ const DEFAULTS = {
|
|
|
555
555
|
* and pruning, which needs to be able to notice an absence.
|
|
556
556
|
*/
|
|
557
557
|
subscriptions: [],
|
|
558
|
+
/*
|
|
559
|
+
* A subscription also takes `keep` and `keepDays`, which are the same rules
|
|
560
|
+
* a watched folder and a scheduled source retire under and are applied by
|
|
561
|
+
* the same code.
|
|
562
|
+
*
|
|
563
|
+
* They answer a different question from `prune`. Pruning is about the
|
|
564
|
+
* publisher — it stopped offering this, so let it go — and needs a complete
|
|
565
|
+
* listing to mean anything, which is why it applies to a catalog and not to
|
|
566
|
+
* an RSS feed. These are about the disk: a feed publishing weekly leaves a
|
|
567
|
+
* copy behind every week and goes on listing all of them, and age is age
|
|
568
|
+
* however short the list is.
|
|
569
|
+
*
|
|
570
|
+
* Retirement runs only after something new has landed, and never removes
|
|
571
|
+
* the newest copy.
|
|
572
|
+
*/
|
|
558
573
|
/**
|
|
559
574
|
* How often to re-check whether the sources archives were built from have
|
|
560
575
|
* changed, in seconds. Zero disables it. A check is one HEAD request or stat
|
package/src/retention.js
CHANGED
|
@@ -99,6 +99,7 @@ export function expired({ family, keep, keepDays, now = Date.now() }) {
|
|
|
99
99
|
* @param {number} [options.keep] - How many of the newest to hold.
|
|
100
100
|
* @param {number} [options.keepDays] - How old a build may get, in days.
|
|
101
101
|
* @param {string} options.label - How to name this family in the log.
|
|
102
|
+
* @param {boolean} [options.requireComplete] - Wait for the newest to be whole.
|
|
102
103
|
* @param {number} [options.now] - The current time, for testing.
|
|
103
104
|
* @returns {Promise<string[]>} - The infohashes removed.
|
|
104
105
|
*/
|
|
@@ -109,8 +110,23 @@ export async function retire({
|
|
|
109
110
|
keep,
|
|
110
111
|
keepDays,
|
|
111
112
|
label,
|
|
113
|
+
requireComplete = false,
|
|
112
114
|
now,
|
|
113
115
|
}) {
|
|
116
|
+
// Where the new copy is a download rather than a file that already exists.
|
|
117
|
+
//
|
|
118
|
+
// A watched folder and a scheduled source hand over an archive that is
|
|
119
|
+
// whole: the file was there, or the fetch finished. A subscription does not
|
|
120
|
+
// — it joins a torrent, and the data arrives hours later. Retiring on the
|
|
121
|
+
// join would delete last week's complete copy the moment this week's was
|
|
122
|
+
// announced, leaving nothing complete for the length of the download.
|
|
123
|
+
//
|
|
124
|
+
// So `keep: 1` here means "the last complete copy", which is the only
|
|
125
|
+
// reading of it that is safe.
|
|
126
|
+
if (requireComplete && entry?.complete !== true) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
const doomed = expired({ family, keep, keepDays, now });
|
|
115
131
|
if (doomed.length === 0) return [];
|
|
116
132
|
|
package/src/subscriptions.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseFeed } from './feed.js';
|
|
2
|
+
import { retains, retire } from './retention.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Follows other nodes' feeds and picks up what they publish.
|
|
@@ -174,6 +175,12 @@ export class SubscriptionManager {
|
|
|
174
175
|
break;
|
|
175
176
|
}
|
|
176
177
|
}
|
|
178
|
+
|
|
179
|
+
// The disk side of following a feed. Pruning cannot apply here — absence
|
|
180
|
+
// from a bounded feed proves nothing — but age applies whatever the list
|
|
181
|
+
// is, which is what makes this the answer for a feed that publishes
|
|
182
|
+
// weekly for ever.
|
|
183
|
+
await this.#retire(subscription);
|
|
177
184
|
return added;
|
|
178
185
|
}
|
|
179
186
|
|
|
@@ -252,9 +259,56 @@ export class SubscriptionManager {
|
|
|
252
259
|
}
|
|
253
260
|
|
|
254
261
|
await this.#prune(subscription, document, archives);
|
|
262
|
+
await this.#retire(subscription);
|
|
255
263
|
return added;
|
|
256
264
|
}
|
|
257
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Removes what this feed's archives have outgrown.
|
|
268
|
+
*
|
|
269
|
+
* Separate from pruning, and answering a different question. Pruning is
|
|
270
|
+
* about the *publisher*: it has stopped offering this, so let it go. This is
|
|
271
|
+
* about the disk: a feed publishing weekly leaves a copy behind every week,
|
|
272
|
+
* and the publisher will go on listing all of them.
|
|
273
|
+
*
|
|
274
|
+
* That distinction is why an RSS feed can have this and cannot have pruning.
|
|
275
|
+
* Absence from a bounded feed is not evidence that anything was withdrawn —
|
|
276
|
+
* planet.openstreetmap.org lists five dumps and says nothing about the
|
|
277
|
+
* hundreds before them — but age is age however short the list is.
|
|
278
|
+
*
|
|
279
|
+
* Only after something new has landed, and never the newest copy, which are
|
|
280
|
+
* the same guards a scheduled source retires under. See `retire`.
|
|
281
|
+
* @param {object} subscription - The subscription.
|
|
282
|
+
* @returns {Promise<void>} - Resolves once anything due has gone.
|
|
283
|
+
*/
|
|
284
|
+
async #retire(subscription) {
|
|
285
|
+
if (!retains(subscription)) return;
|
|
286
|
+
|
|
287
|
+
// Only what this feed brought in. An archive built here, added by hand, or
|
|
288
|
+
// taken from another peer is not this subscription's to remove.
|
|
289
|
+
const family = this.#library.catalog
|
|
290
|
+
.list()
|
|
291
|
+
.filter((entry) => entry.source?.subscription === subscription.url);
|
|
292
|
+
if (family.length === 0) return;
|
|
293
|
+
|
|
294
|
+
// Every poll, not only the polls that took something. What is being waited
|
|
295
|
+
// for is the newest copy *finishing*, which happens hours after the poll
|
|
296
|
+
// that started it — so retiring only on the way in would either act far
|
|
297
|
+
// too early or never act at all.
|
|
298
|
+
await retire({
|
|
299
|
+
library: this.#library,
|
|
300
|
+
family,
|
|
301
|
+
entry: family[0],
|
|
302
|
+
keep: subscription.keep,
|
|
303
|
+
keepDays: subscription.keepDays,
|
|
304
|
+
label: `[sync] ${subscription.url}`,
|
|
305
|
+
// The newest copy here is a download rather than a file that was already
|
|
306
|
+
// there, so nothing goes until it is whole. That is what makes `keep: 1`
|
|
307
|
+
// mean "the last complete copy".
|
|
308
|
+
requireComplete: true,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
258
312
|
/**
|
|
259
313
|
* Drops archives this subscription no longer lists.
|
|
260
314
|
*
|
package/src/web/index.html
CHANGED
|
@@ -2419,6 +2419,14 @@
|
|
|
2419
2419
|
checkNow,
|
|
2420
2420
|
} = spec;
|
|
2421
2421
|
rowEditorColumns[key] = columns;
|
|
2422
|
+
// Where this editor's rows belong, and what they are on top of being
|
|
2423
|
+
// rows. Two editors can be two views of one array — feeds and peers
|
|
2424
|
+
// are both subscriptions — so the panel's own key is a DOM name, and
|
|
2425
|
+
// this is where its records are written.
|
|
2426
|
+
rowEditorTargets[key] = {
|
|
2427
|
+
configKey: spec.configKey ?? key,
|
|
2428
|
+
stamp: spec.stamp ?? {},
|
|
2429
|
+
};
|
|
2422
2430
|
// Kept so a save can put back what this editor never showed. An entry
|
|
2423
2431
|
// holds more than there are columns for it — a watch folder's
|
|
2424
2432
|
// pieceLength, a subscription's savePath — and a save that rebuilt each
|
|
@@ -2433,7 +2441,9 @@
|
|
|
2433
2441
|
<td>
|
|
2434
2442
|
${
|
|
2435
2443
|
column.options
|
|
2436
|
-
? `<select data-field="${column.field}"
|
|
2444
|
+
? `<select data-field="${column.field}"${
|
|
2445
|
+
column.onlyFor ? ` data-only-for="${column.onlyFor}"` : ''
|
|
2446
|
+
} style="width:${column.wide ? '17rem' : '9rem'}">
|
|
2437
2447
|
${column.options
|
|
2438
2448
|
.map(
|
|
2439
2449
|
([option, label]) =>
|
|
@@ -2445,6 +2455,7 @@
|
|
|
2445
2455
|
</select>`
|
|
2446
2456
|
: `<input
|
|
2447
2457
|
data-field="${column.field}"
|
|
2458
|
+
${column.onlyFor ? `data-only-for="${column.onlyFor}"` : ''}
|
|
2448
2459
|
${column.secret ? 'type="password" autocomplete="off"' : ''}
|
|
2449
2460
|
placeholder="${escapeHtml(column.placeholder ?? '')}"
|
|
2450
2461
|
style="width:${column.wide ? '17rem' : '9rem'}"
|
|
@@ -2508,6 +2519,40 @@
|
|
|
2508
2519
|
const tbody = panel.querySelector('tbody');
|
|
2509
2520
|
const note = panel.querySelector('[data-role="note"]');
|
|
2510
2521
|
|
|
2522
|
+
/**
|
|
2523
|
+
* Greys out the fields the chosen protocol does not read.
|
|
2524
|
+
*
|
|
2525
|
+
* The two halves of a subscription are not the same thing wearing
|
|
2526
|
+
* different clothes. `newest` caps how many items a bounded feed hands
|
|
2527
|
+
* over and means nothing to a catalog, which lists everything; `prune`
|
|
2528
|
+
* needs a complete listing to make absence meaningful and so cannot
|
|
2529
|
+
* apply to a feed. Offering both to both invites a setting that is
|
|
2530
|
+
* quietly ignored, which is worse than not offering it.
|
|
2531
|
+
*
|
|
2532
|
+
* Left alone on "auto", since the protocol is then decided by the URL
|
|
2533
|
+
* and this cannot know which it will be.
|
|
2534
|
+
* @param {HTMLElement} row - The row to update.
|
|
2535
|
+
* @returns {void}
|
|
2536
|
+
*/
|
|
2537
|
+
const applyProtocol = (row) => {
|
|
2538
|
+
const chosen = row.querySelector('[data-field="protocol"]')?.value;
|
|
2539
|
+
for (const field of row.querySelectorAll('[data-only-for]')) {
|
|
2540
|
+
const belongs = field.dataset.onlyFor;
|
|
2541
|
+
const off = Boolean(chosen) && chosen !== belongs;
|
|
2542
|
+
field.disabled = off;
|
|
2543
|
+
field.title = off
|
|
2544
|
+
? `Only used by the ${belongs === 'rss' ? 'RSS' : 'catalog API'} protocol`
|
|
2545
|
+
: '';
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2549
|
+
for (const row of tbody.querySelectorAll('tr')) applyProtocol(row);
|
|
2550
|
+
panel.addEventListener('change', (event) => {
|
|
2551
|
+
if (event.target.dataset?.field === 'protocol') {
|
|
2552
|
+
applyProtocol(event.target.closest('tr'));
|
|
2553
|
+
}
|
|
2554
|
+
});
|
|
2555
|
+
|
|
2511
2556
|
panel.onclick = async (event) => {
|
|
2512
2557
|
const token = event.target.dataset?.token;
|
|
2513
2558
|
if (token) {
|
|
@@ -2662,6 +2707,7 @@
|
|
|
2662
2707
|
for (const [key, columns] of Object.entries(specs)) {
|
|
2663
2708
|
const panel = document.querySelector(`[data-row-editor="${key}"]`);
|
|
2664
2709
|
if (!panel) continue;
|
|
2710
|
+
const target = rowEditorTargets[key] ?? { configKey: key, stamp: {} };
|
|
2665
2711
|
const originals = rowEditorRows[key] ?? [];
|
|
2666
2712
|
const records = [...panel.querySelectorAll('tbody tr')]
|
|
2667
2713
|
.map((row) => {
|
|
@@ -2675,8 +2721,17 @@
|
|
|
2675
2721
|
origin === undefined ? {} : (originals[Number(origin)] ?? {}),
|
|
2676
2722
|
);
|
|
2677
2723
|
})
|
|
2678
|
-
.filter((record) => record[columns[0].field])
|
|
2679
|
-
|
|
2724
|
+
.filter((record) => record[columns[0].field])
|
|
2725
|
+
// What the section says about every row in it. A row in the feeds
|
|
2726
|
+
// table is an RSS subscription because of where it is, which is
|
|
2727
|
+
// the point of having two tables rather than a dropdown.
|
|
2728
|
+
.map((record) => ({ ...record, ...target.stamp }));
|
|
2729
|
+
|
|
2730
|
+
// Appended, not assigned: several editors may write one setting.
|
|
2731
|
+
updates[target.configKey] = [
|
|
2732
|
+
...(updates[target.configKey] ?? []),
|
|
2733
|
+
...records,
|
|
2734
|
+
];
|
|
2680
2735
|
}
|
|
2681
2736
|
return updates;
|
|
2682
2737
|
}
|
|
@@ -3168,6 +3223,7 @@
|
|
|
3168
3223
|
// ── Settings ──────────────────────────────────────────────────────────
|
|
3169
3224
|
let restartKeys = new Set();
|
|
3170
3225
|
let rowEditorColumns = {};
|
|
3226
|
+
let rowEditorTargets = {};
|
|
3171
3227
|
// What each editor was rendered from, so a save can put back the fields
|
|
3172
3228
|
// it never showed.
|
|
3173
3229
|
let rowEditorRows = {};
|
|
@@ -3229,6 +3285,7 @@
|
|
|
3229
3285
|
// are lists of small records, which is exactly what a torrent client
|
|
3230
3286
|
// gives a grid for, and what a textarea full of braces is worst at.
|
|
3231
3287
|
rowEditorColumns = {};
|
|
3288
|
+
rowEditorTargets = {};
|
|
3232
3289
|
rowEditorRows = {};
|
|
3233
3290
|
await renderTokenEditor(body);
|
|
3234
3291
|
renderHookEditor(body, config, restartKeys);
|
|
@@ -3460,35 +3517,100 @@
|
|
|
3460
3517
|
</div>`;
|
|
3461
3518
|
body.append(feedGlobals);
|
|
3462
3519
|
|
|
3520
|
+
// Which table a subscription belongs in — the same question the
|
|
3521
|
+
// subscription manager asks itself: an explicit protocol decides it, and
|
|
3522
|
+
// otherwise the shape of the URL does.
|
|
3523
|
+
const isPeer = (row) =>
|
|
3524
|
+
row.protocol === 'api' ||
|
|
3525
|
+
(!row.protocol && /\/api\/catalog\/?$/.test(row.url ?? ''));
|
|
3526
|
+
const following = config.subscriptions ?? [];
|
|
3527
|
+
|
|
3463
3528
|
renderRowEditor({
|
|
3464
3529
|
into: body,
|
|
3465
|
-
key: '
|
|
3466
|
-
|
|
3530
|
+
key: 'feeds',
|
|
3531
|
+
configKey: 'subscriptions',
|
|
3532
|
+
// A row here is RSS because of the table it is in, which is the
|
|
3533
|
+
// point of two tables rather than a dropdown nobody reads.
|
|
3534
|
+
stamp: { protocol: 'rss' },
|
|
3535
|
+
title: 'RSS feeds',
|
|
3467
3536
|
blurb:
|
|
3468
|
-
'
|
|
3469
|
-
'
|
|
3470
|
-
'
|
|
3471
|
-
'
|
|
3472
|
-
'
|
|
3473
|
-
'everything", which is what makes reconciling possible. A token, ' +
|
|
3474
|
-
'where the peer issued one, may get you more than it publishes ' +
|
|
3475
|
-
'to the world.',
|
|
3537
|
+
'Any feed that carries torrents — another swarm node publishes ' +
|
|
3538
|
+
'one, and so does planet.openstreetmap.org for the planet dumps. ' +
|
|
3539
|
+
'A feed says "here is what is new" and is bounded by whoever ' +
|
|
3540
|
+
'publishes it, so a node offline long enough misses things for ' +
|
|
3541
|
+
'good, and an absence from one means nothing at all.',
|
|
3476
3542
|
columns: [
|
|
3477
3543
|
{
|
|
3478
3544
|
field: 'url',
|
|
3479
|
-
label: 'Feed
|
|
3480
|
-
placeholder: 'https://
|
|
3545
|
+
label: 'Feed URL',
|
|
3546
|
+
placeholder: 'https://planet.openstreetmap.org/pbf/planet-pbf-rss.xml',
|
|
3481
3547
|
wide: true,
|
|
3482
3548
|
},
|
|
3483
3549
|
{
|
|
3484
|
-
field: '
|
|
3485
|
-
label: '
|
|
3550
|
+
field: 'mode',
|
|
3551
|
+
label: 'Take as',
|
|
3486
3552
|
options: [
|
|
3487
|
-
['', '
|
|
3488
|
-
['
|
|
3489
|
-
['api', 'catalog API'],
|
|
3553
|
+
['cache', 'cache — on demand'],
|
|
3554
|
+
['mirror', 'mirror — whole copy'],
|
|
3490
3555
|
],
|
|
3491
3556
|
},
|
|
3557
|
+
{ field: 'newest', label: 'Items per check', placeholder: '1', number: true },
|
|
3558
|
+
{
|
|
3559
|
+
field: 'categories',
|
|
3560
|
+
label: 'Categories',
|
|
3561
|
+
placeholder: 'from-peer, planet',
|
|
3562
|
+
list: true,
|
|
3563
|
+
},
|
|
3564
|
+
{ field: 'filter', label: 'Name filter', placeholder: 'terrain' },
|
|
3565
|
+
{ field: 'savePath', label: 'Save to', placeholder: 'the default' },
|
|
3566
|
+
{ field: 'token', label: 'Token', placeholder: 'if issued one', secret: true },
|
|
3567
|
+
{ field: 'keep', label: 'Copies to keep', placeholder: 'all', number: true },
|
|
3568
|
+
{ field: 'keepDays', label: 'Keep for (days)', placeholder: 'for ever', number: true },
|
|
3569
|
+
{
|
|
3570
|
+
field: 'enabled',
|
|
3571
|
+
label: 'On',
|
|
3572
|
+
boolean: true,
|
|
3573
|
+
options: [
|
|
3574
|
+
['', 'yes'],
|
|
3575
|
+
['false', 'no'],
|
|
3576
|
+
],
|
|
3577
|
+
},
|
|
3578
|
+
],
|
|
3579
|
+
rows: following.filter((row) => !isPeer(row)),
|
|
3580
|
+
checkNow: '/api/subscriptions/refresh',
|
|
3581
|
+
peerPreview: true,
|
|
3582
|
+
footnote:
|
|
3583
|
+
'Items per check is 1 by default and counts from the newest, ' +
|
|
3584
|
+
'because a feed listing five planet dumps is four hundred ' +
|
|
3585
|
+
'gigabytes if you take the lot; 0 takes everything it lists. ' +
|
|
3586
|
+
'Copies to keep and Keep for are about your disk rather than the ' +
|
|
3587
|
+
'publisher: a feed publishing weekly leaves a copy behind every ' +
|
|
3588
|
+
'week and goes on listing all of them. Both run only after ' +
|
|
3589
|
+
'something new has landed, and neither removes the newest copy. ' +
|
|
3590
|
+
'There is no dropping here — absence from a bounded feed is not ' +
|
|
3591
|
+
'evidence that anything was withdrawn, which is what a catalogue ' +
|
|
3592
|
+
'is for.',
|
|
3593
|
+
});
|
|
3594
|
+
|
|
3595
|
+
renderRowEditor({
|
|
3596
|
+
into: body,
|
|
3597
|
+
key: 'peers',
|
|
3598
|
+
configKey: 'subscriptions',
|
|
3599
|
+
stamp: { protocol: 'api' },
|
|
3600
|
+
title: 'Remote nodes',
|
|
3601
|
+
blurb:
|
|
3602
|
+
'Another swarm node, followed through its catalogue rather than ' +
|
|
3603
|
+
'its feed. A catalogue says "here is everything", which is what ' +
|
|
3604
|
+
'makes reconciling possible — and dropping, which needs to be ' +
|
|
3605
|
+
'able to notice an absence. A token, where the peer issued one, ' +
|
|
3606
|
+
'may get you more than it publishes to the world.',
|
|
3607
|
+
columns: [
|
|
3608
|
+
{
|
|
3609
|
+
field: 'url',
|
|
3610
|
+
label: 'Catalogue URL',
|
|
3611
|
+
placeholder: 'https://peer.example.org/api/catalog',
|
|
3612
|
+
wide: true,
|
|
3613
|
+
},
|
|
3492
3614
|
{
|
|
3493
3615
|
field: 'mode',
|
|
3494
3616
|
label: 'Take as',
|
|
@@ -3497,19 +3619,14 @@
|
|
|
3497
3619
|
['mirror', 'mirror — whole copy'],
|
|
3498
3620
|
],
|
|
3499
3621
|
},
|
|
3500
|
-
{
|
|
3501
|
-
field: 'newest',
|
|
3502
|
-
label: 'Items per check',
|
|
3503
|
-
placeholder: '1',
|
|
3504
|
-
number: true,
|
|
3505
|
-
},
|
|
3506
3622
|
{
|
|
3507
3623
|
field: 'categories',
|
|
3508
3624
|
label: 'Categories',
|
|
3509
|
-
placeholder: 'from-peer
|
|
3625
|
+
placeholder: 'from-peer',
|
|
3510
3626
|
list: true,
|
|
3511
3627
|
},
|
|
3512
3628
|
{ field: 'filter', label: 'Name filter', placeholder: 'terrain' },
|
|
3629
|
+
{ field: 'savePath', label: 'Save to', placeholder: 'the default' },
|
|
3513
3630
|
{ field: 'token', label: 'Token', placeholder: 'if issued one', secret: true },
|
|
3514
3631
|
{
|
|
3515
3632
|
field: 'prune',
|
|
@@ -3521,6 +3638,8 @@
|
|
|
3521
3638
|
['delete', 'forget and delete'],
|
|
3522
3639
|
],
|
|
3523
3640
|
},
|
|
3641
|
+
{ field: 'keep', label: 'Copies to keep', placeholder: 'all', number: true },
|
|
3642
|
+
{ field: 'keepDays', label: 'Keep for (days)', placeholder: 'for ever', number: true },
|
|
3524
3643
|
{
|
|
3525
3644
|
field: 'enabled',
|
|
3526
3645
|
label: 'On',
|
|
@@ -3531,7 +3650,7 @@
|
|
|
3531
3650
|
],
|
|
3532
3651
|
},
|
|
3533
3652
|
],
|
|
3534
|
-
rows:
|
|
3653
|
+
rows: following.filter(isPeer),
|
|
3535
3654
|
checkNow: '/api/subscriptions/refresh',
|
|
3536
3655
|
peerPreview: true,
|
|
3537
3656
|
footnote:
|
|
@@ -3540,10 +3659,7 @@
|
|
|
3540
3659
|
'every archive the peer lists. Dropping stays off unless chosen, ' +
|
|
3541
3660
|
'only ever considers archives this peer sent, and never acts on a ' +
|
|
3542
3661
|
'filtered or partial view — watch a new peer on "report only" ' +
|
|
3543
|
-
'before trusting it with anything more.
|
|
3544
|
-
'default and counts from the newest, because a feed listing five ' +
|
|
3545
|
-
'planet dumps is four hundred gigabytes if you take the lot; 0 ' +
|
|
3546
|
-
'takes everything it lists.',
|
|
3662
|
+
'before trusting it with anything more.',
|
|
3547
3663
|
});
|
|
3548
3664
|
|
|
3549
3665
|
for (const [key, value] of Object.entries(config)) {
|