pmtiles-swarm 0.17.1 → 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 CHANGED
@@ -2,6 +2,14 @@
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.
5
13
  - **The default trackers now include WebSocket ones.** They were two `udp://` entries, so an
6
14
  archive created with stock configuration was undiscoverable from a browser — healthy in a
7
15
  desktop client, invisible from a page, with nothing in either to say why. A browser speaks
@@ -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.17.1",
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
@@ -613,6 +613,15 @@ const DEFAULTS = {
613
613
  * ever touch archives this same folder imported, and never remove the
614
614
  * newest build however old it gets.
615
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
+ *
616
625
  * `match` is a glob tested against the filename, for a folder holding more
617
626
  * than one kind of build. Categories are decided per entry, so several
618
627
  * entries can watch one folder and each take only its own archives:
@@ -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 attempts = [
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
- ['symlink', () => fs.symlink(target, link)],
36
- ['hard link', () => fs.link(target, link)],
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
- console.log(
46
- `${label} latest -> ${path.basename(target)}` +
47
- (kind === 'symlink' ? '' : ` (${kind})`),
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
@@ -214,6 +214,10 @@ export class WatchManager {
214
214
  target: entry.retainedAt ?? path.join(entry.savePath, entry.name),
215
215
  name: folder.latestLink,
216
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,
217
221
  });
218
222
  }
219
223
 
@@ -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',