pmtiles-swarm 0.17.0 → 0.18.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 +19 -1
- package/docs/engines.md +12 -12
- package/docs/publishing.md +18 -0
- package/package.json +1 -1
- package/src/config.js +26 -0
- package/src/latest-link.js +22 -8
- package/src/watch.js +19 -3
- package/src/web/index.html +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## master
|
|
4
4
|
### ✨ Features and improvements
|
|
5
|
+
- **A watched folder can ask for its stable name to be a hard link.**
|
|
6
|
+
`latestLinkType: "hard"` reverses the order the two kinds are attempted in,
|
|
7
|
+
for a folder whose archives are read *through* that name rather than followed
|
|
8
|
+
to see where it points. A hard link still resolves after the build it names
|
|
9
|
+
is retired; a symlink is left pointing at nothing, which a tile endpoint
|
|
10
|
+
reports as a missing archive while the bytes are still on the disk. The other
|
|
11
|
+
kind remains the fallback in both directions, and the log now always says
|
|
12
|
+
which was made rather than only mentioning the unexpected one.
|
|
13
|
+
- **The default trackers now include WebSocket ones.** They were two `udp://` entries, so an
|
|
14
|
+
archive created with stock configuration was undiscoverable from a browser — healthy in a
|
|
15
|
+
desktop client, invisible from a page, with nothing in either to say why. A browser speaks
|
|
16
|
+
WebRTC only and has no DHT, PeX or local discovery to fall back on, so `wss://` is not
|
|
17
|
+
redundancy with the rest of the list, it is the whole of that path. Two are listed because it
|
|
18
|
+
has no backstop. Every entry was checked for a completed handshake before being added.
|
|
5
19
|
- _...Add new stuff here..._
|
|
6
20
|
|
|
7
21
|
### 🐞 Bug fixes
|
|
8
|
-
-
|
|
22
|
+
- **Retention no longer reaches across a folder's other entries.** A watched folder's family was
|
|
23
|
+
built from the directory alone, so several entries sharing one — which `match` exists to make
|
|
24
|
+
possible — were treated as a single family. With `keep: 1`, importing this week's `monthly`
|
|
25
|
+
retired `10yrplus` and deleted its data. The family is now scoped by the entry's glob as well
|
|
26
|
+
as its path. Introduced in 0.17.0; anything affected must be regenerated.
|
|
9
27
|
|
|
10
28
|
## 0.17.0
|
|
11
29
|
### ✨ Features and improvements
|
package/docs/engines.md
CHANGED
|
@@ -285,31 +285,31 @@ load becomes swarm capacity instead of passing through the same pipe.
|
|
|
285
285
|
This is a configuration requirement, not an automatic one, and it is easy to miss because
|
|
286
286
|
everything looks healthy without it.
|
|
287
287
|
|
|
288
|
-
The default
|
|
288
|
+
The default list carries both kinds:
|
|
289
289
|
|
|
290
290
|
```json
|
|
291
291
|
"trackers": [
|
|
292
292
|
"udp://tracker.opentrackr.org:1337/announce",
|
|
293
|
-
"udp://tracker.torrent.eu.org:451/announce"
|
|
293
|
+
"udp://tracker.torrent.eu.org:451/announce",
|
|
294
|
+
"udp://tracker-udp.gbitt.info:80/announce",
|
|
295
|
+
"wss://tracker.openwebtorrent.com",
|
|
296
|
+
"wss://tracker.webtorrent.dev"
|
|
294
297
|
]
|
|
295
298
|
```
|
|
296
299
|
|
|
297
|
-
**A browser cannot use
|
|
300
|
+
**A browser cannot use the UDP ones, and cannot use the DHT.** Both need UDP sockets and raw
|
|
298
301
|
TCP, which a page does not have. A browser's only route to discovery is a `wss://` tracker — so a
|
|
299
302
|
torrent announcing to UDP trackers alone is one no browser can find a peer for, however many
|
|
300
303
|
WebTorrent nodes are seeding it. WebTorrent's own default WebSocket trackers do not fill this gap
|
|
301
304
|
either: they are compiled into its **browser** bundle, and a Node client adds nothing.
|
|
302
305
|
|
|
303
|
-
|
|
306
|
+
Two `wss://` entries are listed rather than one because that path has no backstop: when the UDP
|
|
307
|
+
side loses a tracker there are still the others, the DHT, PeX and local discovery, and when the
|
|
308
|
+
WebSocket side loses one a browser has nothing left.
|
|
304
309
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
"udp://tracker.torrent.eu.org:451/announce",
|
|
309
|
-
"wss://tracker.openwebtorrent.com",
|
|
310
|
-
"wss://tracker.webtorrent.dev"
|
|
311
|
-
]
|
|
312
|
-
```
|
|
310
|
+
If you replace this list, keep at least one `wss://` entry — and check it answers, rather than
|
|
311
|
+
copying one from a list. Public WebSocket trackers come and go, and a dead one fails in exactly
|
|
312
|
+
the way this section describes: silently, and only for browsers.
|
|
313
313
|
|
|
314
314
|
Trackers live outside the torrent's `info` dictionary, so adding them **does not change the
|
|
315
315
|
infohash** — but it only applies to torrents created after the change. An existing archive keeps
|
package/docs/publishing.md
CHANGED
|
@@ -377,6 +377,24 @@ A new `.pmtiles` appearing in a watched folder is imported automatically.
|
|
|
377
377
|
|
|
378
378
|
Editable in **Settings → Monitored folders** as a table, rather than by hand.
|
|
379
379
|
|
|
380
|
+
### What kind of link the stable name is
|
|
381
|
+
|
|
382
|
+
`latestLink` is made as a symlink by default. Set `latestLinkType: "hard"` on
|
|
383
|
+
the folder when something reads the archive *through* that name — a tile
|
|
384
|
+
server, or anything else that opens it rather than following it to see where it
|
|
385
|
+
points.
|
|
386
|
+
|
|
387
|
+
The difference only shows when the build the name refers to goes away. A hard
|
|
388
|
+
link is another name for the same bytes, so it still resolves; a symlink is
|
|
389
|
+
left pointing at nothing. Retention repoints the name as it retires a build, so
|
|
390
|
+
neither case arises from ordinary housekeeping — but a rebuild that fails
|
|
391
|
+
between the two steps leaves a reader working in one arrangement and broken in
|
|
392
|
+
the other.
|
|
393
|
+
|
|
394
|
+
The other kind stays the fallback either way: Windows refuses symlinks without
|
|
395
|
+
elevation, and a hard link cannot cross a filesystem, so the name is still made
|
|
396
|
+
whichever was asked for. The log says which it got.
|
|
397
|
+
|
|
380
398
|
### One folder, several kinds of build
|
|
381
399
|
|
|
382
400
|
Categories and retention are decided per entry, not per directory. A generator
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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/config.js
CHANGED
|
@@ -236,10 +236,27 @@ const DEFAULTS = {
|
|
|
236
236
|
*
|
|
237
237
|
* Only applies to torrents created here. Joining an existing one uses the
|
|
238
238
|
* trackers that torrent already carries.
|
|
239
|
+
*
|
|
240
|
+
* The `wss://` entries are not redundancy with the others — they are the
|
|
241
|
+
* only ones a browser can use. A page speaks WebRTC and nothing else: it
|
|
242
|
+
* cannot reach a UDP or HTTP tracker, and it has no DHT, PeX or local
|
|
243
|
+
* discovery to fall back on. An archive announced only to `udp://` trackers
|
|
244
|
+
* is perfectly healthy in a desktop client and invisible from a browser,
|
|
245
|
+
* with nothing in either to say why. Two are listed because that single
|
|
246
|
+
* path has no backstop.
|
|
247
|
+
*
|
|
248
|
+
* Keep this list short and answering. A tracker that no longer resolves
|
|
249
|
+
* costs an announce attempt per interval per torrent and finds nobody —
|
|
250
|
+
* unlike one that is merely unreachable from here, which can still
|
|
251
|
+
* introduce peers on networks that can reach it, since the list travels
|
|
252
|
+
* with the torrent.
|
|
239
253
|
*/
|
|
240
254
|
trackers: [
|
|
241
255
|
'udp://tracker.opentrackr.org:1337/announce',
|
|
242
256
|
'udp://tracker.torrent.eu.org:451/announce',
|
|
257
|
+
'udp://tracker-udp.gbitt.info:80/announce',
|
|
258
|
+
'wss://tracker.openwebtorrent.com',
|
|
259
|
+
'wss://tracker.webtorrent.dev',
|
|
243
260
|
],
|
|
244
261
|
/**
|
|
245
262
|
* Copy every .torrent we create into this directory as well.
|
|
@@ -596,6 +613,15 @@ const DEFAULTS = {
|
|
|
596
613
|
* ever touch archives this same folder imported, and never remove the
|
|
597
614
|
* newest build however old it gets.
|
|
598
615
|
*
|
|
616
|
+
* `latestLinkType` picks how `latestLink` is made: `symbolic` (the default),
|
|
617
|
+
* or `hard` for a name something reads the archive through. A hard link
|
|
618
|
+
* still resolves after the build it names is retired, where a symlink is
|
|
619
|
+
* left pointing at nothing — so a tile endpoint opening that name keeps
|
|
620
|
+
* working across a rebuild either way, and only one of them keeps working if
|
|
621
|
+
* the rebuild fails halfway. The other kind stays the fallback in both
|
|
622
|
+
* directions, since Windows refuses symlinks without elevation and a hard
|
|
623
|
+
* link cannot cross a filesystem.
|
|
624
|
+
*
|
|
599
625
|
* `match` is a glob tested against the filename, for a folder holding more
|
|
600
626
|
* than one kind of build. Categories are decided per entry, so several
|
|
601
627
|
* entries can watch one folder and each take only its own archives:
|
package/src/latest-link.js
CHANGED
|
@@ -20,21 +20,35 @@ import path from 'node:path';
|
|
|
20
20
|
* this names is eventually deleted: a symlink is left dangling, while a hard
|
|
21
21
|
* link keeps the bytes alive until it too is gone. Retention never removes the
|
|
22
22
|
* newest build, so neither case arises from this node's own housekeeping.
|
|
23
|
+
*
|
|
24
|
+
* `type` chooses which is preferred, because which one is better depends on
|
|
25
|
+
* what opens the name. Something reading the archive through this link — a
|
|
26
|
+
* tile endpoint, say — is better served by a hard link: if the build it names
|
|
27
|
+
* is removed before the name is repointed, a hard link still resolves to the
|
|
28
|
+
* bytes while a symlink resolves to nothing. The other order is the default
|
|
29
|
+
* only because it is what has always happened here.
|
|
23
30
|
* @param {object} options - What to link and how to say so.
|
|
24
31
|
* @param {string} options.target - The build the name should resolve to.
|
|
25
32
|
* @param {string} options.name - The stable name, absolute or beside the target.
|
|
26
33
|
* @param {string} options.label - How to name the caller in the log.
|
|
34
|
+
* @param {string} [options.type] - 'symbolic' (default), 'hard', or 'auto'.
|
|
27
35
|
* @returns {Promise<string|undefined>} - The link made, or undefined on failure.
|
|
28
36
|
*/
|
|
29
|
-
export async function linkLatest({ target, name, label }) {
|
|
37
|
+
export async function linkLatest({ target, name, label, type = 'symbolic' }) {
|
|
30
38
|
const link = linkPathFor(target, name);
|
|
31
39
|
|
|
32
|
-
const
|
|
40
|
+
const symbolic = [
|
|
33
41
|
// The type is autodetected from the target on Windows and ignored
|
|
34
42
|
// everywhere else, which is right: the target is always a file here.
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
'symlink',
|
|
44
|
+
() => fs.symlink(target, link),
|
|
37
45
|
];
|
|
46
|
+
const hard = ['hard link', () => fs.link(target, link)];
|
|
47
|
+
|
|
48
|
+
// Both orders keep the other as a fallback rather than failing outright:
|
|
49
|
+
// Windows refuses symlinks without elevation, and a hard link cannot cross a
|
|
50
|
+
// filesystem. Whichever is asked for, the name still gets made.
|
|
51
|
+
const attempts = type === 'hard' ? [hard, symbolic] : [symbolic, hard];
|
|
38
52
|
|
|
39
53
|
for (const [kind, make] of attempts) {
|
|
40
54
|
try {
|
|
@@ -42,10 +56,10 @@ export async function linkLatest({ target, name, label }) {
|
|
|
42
56
|
// per build and the name by definition already exists after the first.
|
|
43
57
|
await fs.rm(link, { force: true });
|
|
44
58
|
await make();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
59
|
+
// Always says which kind. Reporting only the unexpected one meant the
|
|
60
|
+
// common case was silent, and the only way to learn what you had was to
|
|
61
|
+
// list the directory and read the mode bits.
|
|
62
|
+
console.log(`${label} latest -> ${path.basename(target)} (${kind})`);
|
|
49
63
|
return link;
|
|
50
64
|
} catch (error) {
|
|
51
65
|
// Try the next kind rather than giving up on the first refusal; only the
|
package/src/watch.js
CHANGED
|
@@ -134,7 +134,7 @@ export class WatchManager {
|
|
|
134
134
|
return !this.#isLatestLink(file, entry);
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
if (owner) this.#import(file, owner.entry);
|
|
137
|
+
if (owner) this.#import(file, owner.entry, owner.match);
|
|
138
138
|
});
|
|
139
139
|
watcher.on('error', (error) => {
|
|
140
140
|
console.error(`[watch] ${directory}: ${error.message}`);
|
|
@@ -172,9 +172,10 @@ export class WatchManager {
|
|
|
172
172
|
* Imports one archive, guarding against overlapping imports of the same file.
|
|
173
173
|
* @param {string} file - Path to the archive.
|
|
174
174
|
* @param {object} folder - The watch-folder configuration.
|
|
175
|
+
* @param {RegExp|null} [match] - The entry's filename filter, if it has one.
|
|
175
176
|
* @returns {Promise<void>} - Resolves once imported or skipped.
|
|
176
177
|
*/
|
|
177
|
-
async #import(file, folder) {
|
|
178
|
+
async #import(file, folder, match = null) {
|
|
178
179
|
if (this.#importing.has(file)) return;
|
|
179
180
|
this.#importing.add(file);
|
|
180
181
|
try {
|
|
@@ -213,6 +214,10 @@ export class WatchManager {
|
|
|
213
214
|
target: entry.retainedAt ?? path.join(entry.savePath, entry.name),
|
|
214
215
|
name: folder.latestLink,
|
|
215
216
|
label: `[watch] ${folder.path}`,
|
|
217
|
+
// Per folder: a folder whose archives are opened through this
|
|
218
|
+
// name wants a hard link, one that is only a convenience for a
|
|
219
|
+
// person browsing the directory does not care.
|
|
220
|
+
type: folder.latestLinkType,
|
|
216
221
|
});
|
|
217
222
|
}
|
|
218
223
|
|
|
@@ -223,9 +228,20 @@ export class WatchManager {
|
|
|
223
228
|
if (!retains(folder)) return;
|
|
224
229
|
await retire({
|
|
225
230
|
library: this.#library,
|
|
231
|
+
// Scoped by the entry, not just by the directory. Several entries
|
|
232
|
+
// describing one directory is the whole point of `match`, and a family
|
|
233
|
+
// built from the path alone puts every bucket in one: importing
|
|
234
|
+
// this week's `monthly` would retire `10yrplus` under keep:1, deleting
|
|
235
|
+
// an archive that has nothing to do with it. Re-applying the glob is
|
|
236
|
+
// what splits one directory back into the families the config
|
|
237
|
+
// describes.
|
|
226
238
|
family: this.#library.catalog
|
|
227
239
|
.list()
|
|
228
|
-
.filter(
|
|
240
|
+
.filter(
|
|
241
|
+
(candidate) =>
|
|
242
|
+
candidate.source?.watch === folder.path &&
|
|
243
|
+
(!match || match.test(path.basename(candidate.name ?? ''))),
|
|
244
|
+
),
|
|
229
245
|
entry,
|
|
230
246
|
keep: folder.keep,
|
|
231
247
|
keepDays: folder.keepDays,
|
package/src/web/index.html
CHANGED
|
@@ -3427,6 +3427,14 @@
|
|
|
3427
3427
|
label: 'Stable name',
|
|
3428
3428
|
placeholder: 'off',
|
|
3429
3429
|
},
|
|
3430
|
+
{
|
|
3431
|
+
field: 'latestLinkType',
|
|
3432
|
+
label: 'Stable name is a',
|
|
3433
|
+
options: [
|
|
3434
|
+
['', 'symlink'],
|
|
3435
|
+
['hard', 'hard link'],
|
|
3436
|
+
],
|
|
3437
|
+
},
|
|
3430
3438
|
{
|
|
3431
3439
|
field: 'keep',
|
|
3432
3440
|
label: 'Builds to keep',
|