pmtiles-swarm 0.61.0 → 0.63.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
@@ -7,6 +7,229 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.63.0
11
+ ### ✨ Features and improvements
12
+ - **A terrain archive previews as terrain.** An archive whose `encoding` is `terrarium`,
13
+ `mapbox`, or `custom` with all four factors present now opens as hillshade with 3D relief
14
+ rather than as the raster it literally is — a terrain-RGB image drawn as colour says nothing
15
+ about the ground. The pitch ceiling goes to 85°, since MapLibre's default of 60 is not enough
16
+ to look across a landscape, and terrain itself is a control rather than a setting, because
17
+ flat hillshade is easier to compare against a map than a perspective is.
18
+
19
+ No server change was needed. The TileJSON has carried `encoding` and the four custom factors
20
+ for a while, and the preview already fetches it — so this is the page reading what was
21
+ already there. Stacks preview through the same file, which means a terrain stack gets the
22
+ view from its own declared output encoding.
23
+
24
+ `mlt` is not terrain. It travels in the same field and is a vector format, so the check names
25
+ the three encodings it can draw rather than testing that an encoding is set.
26
+
27
+ The raw tiles stay one click away and the map position survives the switch, because a missing
28
+ DEM tile hillshades as flat ground rather than as missing — the only way to see a hole is to
29
+ look at the pixels. A `custom` archive carrying none of its factors is drawn as an ordinary
30
+ raster with a note saying why.
31
+
32
+
33
+ ### 🐞 Bug fixes
34
+ - _...Add new stuff here..._
35
+
36
+ ## 0.62.0
37
+ ### ✨ Features and improvements
38
+ - **Tile stacks: several archives served as one tile endpoint.** A stack is a
39
+ recipe rather than a file — an ordered list of sources, bottom first, with the
40
+ last painting over the ones before it. Sources are named by category, so a
41
+ stack follows a rebuild the way `/latest/<category>/` does, or by infohash
42
+ where it must not move.
43
+
44
+ What ships is the part that needs no image handling: `/stacks/<id>/tiles.json`
45
+ and `/stacks/<id>/{z}/{x}/{y}.<ext>`, answered by the topmost source holding
46
+ the tile. That is enough for the common shape — a regional archive over a
47
+ global one — and it costs nothing per tile beyond the read it would have done
48
+ anyway. `X-Stack-Sources` names which sources were asked and what each said,
49
+ because a stack missing a layer still renders, and flat ocean looks like a
50
+ plausible map rather than like a failure.
51
+
52
+ A recipe asking for masking, height shifts, opacity, blending or a different
53
+ output encoding answers 501 and names the field. Those need a pixel codec,
54
+ which this node does not have yet; approximating them would be worse than
55
+ refusing. See docs/tile-stacks.md.
56
+
57
+ - **A pixel codec, for the parts of a stack that are not passthrough.** `sharp`,
58
+ as an optional dependency probed at first use — the same library tileserver-gl
59
+ uses, so one image stack covers both ends of the pipeline. A node that only
60
+ distributes archives never needs it, and a node without it answers 501 naming
61
+ what to install rather than failing at the first tile.
62
+
63
+ Encoding is **lossless by default and has to be made lossy by name**. A
64
+ terrain-RGB pixel is not a colour: the three channels are the three bytes of
65
+ one height, so a lossy codec that shifts red by one moves the ground by 65
66
+ kilometres. Over an ordinary gradient, lossy WebP is wrong by about 125 km at
67
+ worst where lossless is byte-exact.
68
+
69
+ - **Elevation stacks merge for real.** A stack whose sources mask, shift or
70
+ re-encode is now served rather than refused: each source is decoded to metres,
71
+ masked, adjusted, resampled in float space and painted in the recipe's order,
72
+ then encoded once. A source with no tile at the requested zoom is taken from
73
+ its parent and cropped to the right sub-square, which is what lets a z8 global
74
+ source keep contributing at z14 — the passthrough path cannot do that, because
75
+ a parent's *bytes* are the wrong tile.
76
+
77
+ Two ways to say "no data here": `maskValues` names decoded heights, and
78
+ `maskColors` names pixel colours as `"#rrggbb"` or `[r, g, b]`. The colour form
79
+ is exact, comparing the bytes that were stored; the height form rounds, because
80
+ decoding produces `base + n * interval` in floating point and a mask of `-0.1`
81
+ meets a decoded `-0.09999999999763531`.
82
+
83
+ A tile no source covered answers 404 rather than a slab of nodata, so a client
84
+ overzooms a lower one. That decision is made on the coverage before nodata is
85
+ substituted in — afterwards every pixel holds a real value and there is nothing
86
+ left to test.
87
+
88
+ - **Merged tiles are cached on disk.** A merged tile costs a read of every
89
+ source, a decode each and an encode, and against a cache-mode source those
90
+ reads may go to the swarm — doing that again for a tile somebody already asked
91
+ for is the difference between a map that pans and one that does not. Bounded
92
+ by `stacks.cacheBytes` (2 GiB by default, zero to turn it off), evicted
93
+ least-recently-used, and indexed from disk at startup so a restart does not
94
+ throw the work away.
95
+
96
+ Keyed by the tile's ETag, which already covers the recipe's revision and what
97
+ its sources resolved to — so editing a stack or rebuilding a source produces a
98
+ different key rather than needing anything to remember to invalidate the old
99
+ one. Only the merging path is cached: passthrough already costs one read, and
100
+ keeping its answer would put a second copy of the archive's own bytes beside
101
+ the first.
102
+
103
+ Several requests for the same tile at once run one merge between them, which
104
+ matters because each duplicate would otherwise issue its own reads to every
105
+ source underneath it.
106
+
107
+ - **A stack's TileJSON declares `sparse`.** True by default, which for a stack
108
+ is not a guess: `maxzoom` is the deepest any source reaches, so most of the
109
+ pyramid below it is covered by only some of them. A tile no source covered
110
+ answers 404, which is what makes maplibre-gl-js and maplibre-native overzoom
111
+ the parent rather than draw nothing — the same flag, the same name and the
112
+ same rule tileserver-gl reads. A stack can set `sparse: false` to answer 204
113
+ instead.
114
+
115
+ - **Image stacks composite, with opacity and blend modes.** `space: "rgba"`
116
+ treats a tile as what it looks like rather than as packed numbers: each source
117
+ carries an `opacity` and a `blend` (`normal`, `multiply`, `screen`, `overlay`,
118
+ `darken`, `lighten`), and `maskColors` clears coverage so what is underneath
119
+ shows through. Hillshade over satellite is the case it exists for.
120
+
121
+ The W3C compositing model in full, not the source-over shortcut — the shortcut
122
+ is only correct when the backdrop is opaque, and a hillshade over a satellite
123
+ tile with transparent edges is exactly where that shows. Resampling from a
124
+ parent interpolates with alpha premultiplied, which is what stops a
125
+ transparent pixel dragging its colour into its neighbours and ringing
126
+ everything with a dark halo.
127
+
128
+ Terrain stays lossless; imagery may be compressed as a picture, which is the
129
+ only place the two spaces disagree about encoding.
130
+
131
+ - **A Stacks view in the console.** Every stack, what each source resolved to,
132
+ the zooms each covers, and — kept apart, because they call for different
133
+ things — what is invalid in a recipe, what cannot be served without a codec,
134
+ and whose sources are missing. Sources are listed in the file's order with
135
+ their indices, so the screen and `data/stacks.json` never disagree.
136
+
137
+ The tab is always present, including on a node with no stacks — that is where
138
+ a stack gets made, so hiding it until one exists would make the first one
139
+ unreachable. An empty state says what a stack is and how to add one.
140
+
141
+ - **`savePathLayout: "name"`**, giving each joined archive `<savePath>/<archive name>/`. The
142
+ infohash layout already separated two builds of the same map, but nothing in
143
+ `<savePath>/7fae2931a9269684a7d4ed6e5fdd7d0014e6bcd1/` tells you which map is in it. This is
144
+ the same separation in a directory you can find.
145
+
146
+ The name comes from the metainfo, or from a magnet's `dn=` — every magnet this node hands out
147
+ carries one — and there are three tiers so a placement can never fail to produce a directory.
148
+ A name already held by another archive takes the first eight characters of the infohash as a
149
+ suffix, which is the rebuild case: same name, new infohash. An archive with no usable name
150
+ takes its infohash, which is a bare magnet from somewhere else.
151
+
152
+ The name is written by whoever built the torrent, so it is sanitised down to one path segment
153
+ before it is joined to anything: separators, control characters and the characters Windows
154
+ refuses become `-`, leading and trailing dots go, the Windows device names get out of the way,
155
+ and it is cut to 120 characters. A name that survives none of that falls back to the infohash.
156
+
157
+ The directory is settled when the archive is added and never revised. A name learned later
158
+ over BEP 9 leaves the data where it is rather than moving hundreds of gigabytes to match a
159
+ tidier directory. Changing the setting places new arrivals only — everything already held
160
+ keeps the save path recorded for it, so nothing moves and nothing is re-checked.
161
+
162
+ - **The settings pane has tabs, and the settings in them have names and reasons.** Seven
163
+ groups — Serving tiles, Network, Publishing, Feeds, Engine, Transfers, Security — describing
164
+ 67 settings as labelled fields with help text, beside one tab holding what is not described
165
+ yet. Nested keys are described one at a time, so `libtorrent.resumeDir` is a field with a
166
+ reason rather than a line inside a JSON blob.
167
+
168
+ The tables moved with them. Monitored folders, watched web locations, RSS feeds and remote
169
+ nodes are four ways of answering one question — where archives arrive from — and they now
170
+ sit together under **Feeds** rather than stacked underneath everything else. Tokens went to
171
+ Security, hooks and the speed and seeding limits to Transfers. Save locations went with the
172
+ feeds: a location is where an arriving archive lands, and the four tables above it are what
173
+ choose one. A
174
+ tab is therefore not only schema fields: `Feeds` is four tables and nothing else, which is
175
+ why the tabs are declared rather than derived from the schema.
176
+
177
+ What the node's own RSS says moved the other way, into Publishing. One tab called Feeds
178
+ holding both what this node emits and what it subscribes to was the muddle that made the
179
+ grouping worth doing.
180
+
181
+ Every setting the configuration declares is now described. The six that were left — the
182
+ WebTorrent client, the console login, DHT publishing, tile statistics, the traffic chart and
183
+ automatic rebuilds — were objects rendered as unlabelled JSON, so changing `traffic.keepHours`
184
+ meant editing JSON in a browser. The last tab now removes itself when it holds nothing, and
185
+ comes back on its own the moment a setting exists that the schema does not describe.
186
+
187
+ They come from a schema rather than from hand-written markup, which is what makes the rest
188
+ of it possible. Adding a setting used to be three edits in three places: the control, a
189
+ branch in the save handler, and a line in the skip-list of the generic renderer — where
190
+ forgetting the third showed the field twice and forgetting the second saved nothing. It is
191
+ now one row.
192
+
193
+ Two things fall out of that. A list-valued setting like `trackers` is edited one per line
194
+ instead of as JSON in a textarea. And the restart badge is per setting rather than per
195
+ top-level key, which is the thing the server cannot express: `tiles.maxOpenArchives` is read
196
+ live while `tiles.directoryCacheEntries` is not, and `RESTART_REQUIRED` has to mark the
197
+ whole object because the console used to edit it as one blob.
198
+
199
+ A save sends only what changed. Sending a whole group back would have reported that a
200
+ restart was needed on every press of Save — `JSON.stringify` keeps `null` and drops
201
+ `undefined`, so a group carrying either where the config has the other compares unequal
202
+ against a configuration nobody touched.
203
+
204
+ ### 🐞 Bug fixes
205
+ - **A settings control that could only ever fail.** Describing every setting as a field gave
206
+ `allowHooksFromApi` a checkbox, and that one is refused by `saveConfig` whether it is on or
207
+ off — it decides whether an API token may choose what code the service runs, so it is the
208
+ config file's alone. Toggling it returned an error, and because everything in a save is
209
+ checked before anything is applied, it took the rest of that tab's settings down with it.
210
+
211
+ Fields the config file owns now render disabled with a `config file` badge, and the reader
212
+ skips disabled controls outright, so a stray enabled attribute cannot turn into a rejected
213
+ save. The hooks themselves were never affected: their editor has always disabled itself and
214
+ said why.
215
+
216
+ - **Two settings reported success and changed nothing.** `tiles` and `resumeSaveIntervalSeconds`
217
+ are read while the process starts — the tile reader's directory cache when the store is
218
+ built, the resume timer when it is created — and neither is consulted again. Both were
219
+ absent from `RESTART_REQUIRED`, so the console applied them, said so, and the node went on
220
+ behaving exactly as before. Being told to restart when you need not have is a small cost;
221
+ a setting that lies about taking effect is not.
222
+
223
+ `tiles` is blunter than it could be, deliberately: `maxOpenArchives` beside
224
+ `directoryCacheEntries` really is read live, but the console edits the object as a whole so
225
+ a badge on half of it is not expressible. That resolves when the settings pane grows real
226
+ fields for it.
227
+
228
+ - **`seedingCheckIntervalSeconds` is reloadable, and was neither.** The sweep reads its own
229
+ interval when it starts and there is already a reloader that restarts the sweep, so this
230
+ needed no restart and no new machinery — only to be listed beside the `seeding` object it
231
+ belongs to.
232
+
10
233
  ## 0.61.0
11
234
  ### ✨ Features and improvements
12
235
  - **"Style URL" is now "source URL", because that is what it is.** It goes in a style's
package/NOTICE.md CHANGED
@@ -44,6 +44,19 @@ library is used unmodified through its public API; `src/web/preview.html` is our
44
44
  served the same way from its own `dist`. Used through its documented options; no implementation
45
45
  code is copied.
46
46
 
47
+ ## sharp — Apache-2.0
48
+
49
+ > Copyright 2013 Lovell Fuller and others
50
+ > https://github.com/lovell/sharp
51
+
52
+ The pixel codec behind tile stacks: decoding a tile to samples and encoding the result. An
53
+ optional dependency, probed at first use, so a node that only distributes archives never
54
+ installs it — see `src/codec.js`. Used unmodified through its public API.
55
+
56
+ The prebuilt `@img/sharp-*` packages it resolves carry libvips (LGPL-3.0-or-later) and its own
57
+ dependencies, each under their own terms; sharp links to libvips dynamically and ships it
58
+ unmodified.
59
+
47
60
  ## qBittorrent — GPL-2.0-or-later
48
61
 
49
62
  > https://github.com/qbittorrent/qBittorrent
package/README.md CHANGED
@@ -237,7 +237,7 @@ each monitored folder and each watched web location:
237
237
 
238
238
  ```json
239
239
  "locations": [
240
- { "name": "bulk storage", "path": "M:\_NZB_Finished_Unsorted" },
240
+ { "name": "bulk storage", "path": "M:\\archives\\finished-unsorted" },
241
241
  { "name": "fast", "path": "/mnt/nvme/tiles" }
242
242
  ]
243
243
  ```
@@ -262,7 +262,18 @@ one at the same path is refused with a 409 naming the first. Where that comes up
262
262
  data/torrents-data/7fae2931a9269684a7d4ed6e5fdd7d0014e6bcd1/planet.pmtiles
263
263
  ```
264
264
 
265
- It works from a bare magnet, since the infohash is the one thing a magnet always carries. Flat
265
+ It works from a bare magnet, since the infohash is the one thing a magnet always carries.
266
+ `"savePathLayout": "name"` gives the same separation in a directory you can find without knowing
267
+ the infohash:
268
+
269
+ ```
270
+ data/torrents-data/planet.pmtiles/planet.pmtiles
271
+ ```
272
+
273
+ The name comes from the metainfo, or from a magnet's `dn=` — every magnet this node hands out
274
+ carries one — and is sanitised down to a single path segment before it is used. A second archive
275
+ of the same name takes `planet.pmtiles-7fae2931`, and one joined by a bare magnet, which carries
276
+ no name at all, takes its infohash. Flat
266
277
  stays the default: it is what makes dropping a finished archive into the save path before adding
267
278
  its torrent work, and it keeps a served filename readable. Archives _created_ from a local file
268
279
  are unaffected either way — they keep the file they were made from — and web seed URLs are built
@@ -739,58 +750,65 @@ which the endpoint answers 501.
739
750
 
740
751
  ## API
741
752
 
742
- | Method | Path | Purpose |
743
- | --------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
744
- | `GET` | `/api/status` | Engine health, counts, watched folders, save locations, free space, and whether the swarm can reach this node |
745
- | `GET` | `/api/torrents` | Catalog joined with live swarm state and what is left of each seeding limit |
746
- | `POST` | `/api/torrents` | Add via `{path}`, `{url}`, `{magnet}`, `{torrentUrl}`, or a raw `.torrent` body |
747
- | `DELETE` | `/api/torrents/:infoHash` | Remove (`?deleteData=true` to delete data too) |
748
- | `GET` | `/api/torrents/:infoHash` | One archive, with disk usage and how it is being read |
749
- | `GET` | `/api/torrents/:infoHash/file`, `/magnet` | Download the `.torrent`, or its magnet URI |
750
- | `GET` | `/api/torrents/:infoHash/peers`, `/trackers`, `/content` | Per-peer, per-tracker and per-file detail |
751
- | `GET` | `/api/torrents/:infoHash/pieces` | Which pieces are held, how rare each is, and what peers hold |
752
- | `PATCH` | `/api/torrents/:infoHash/mode` | Switch between mirror and cache |
753
- | `PATCH` | `/api/torrents/:infoHash/categories` | Set, add or remove categories |
754
- | `PATCH` | `/api/torrents/:infoHash/seeding` | Per-archive seeding limit, or "use the global one" |
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 |
757
- | `POST` | `/api/torrents/:infoHash/pause`, `/resume` | Stop offering it, without forgetting it |
758
- | `POST` | `/api/torrents/:infoHash/webseeds` | Add web seeds — does not change the infohash |
759
- | `DELETE` | `/api/torrents/:infoHash/webseeds` | Drop web seeds — likewise |
760
- | `POST` | `/api/torrents/:infoHash/publish` | What this node offers of the archive itself: `serveArchive`, `selfWebSeed`, `publicDownload` |
761
- | `POST` | `/api/torrents/:infoHash/warm` | Pre-fetch a region (`GET` for progress, `DELETE` to cancel) |
762
- | `DELETE` | `/api/torrents/:infoHash/cache` | Reclaim cached pieces, keep the archive |
763
- | `POST` | `/api/torrents/:infoHash/check` | Has the source changed since the torrent was made? |
764
- | `POST` | `/api/torrents/:infoHash/rebuild` | Rebuild from the current source (mints a new infohash) |
765
- | `POST` | `/api/check-origins` | Check every archive with a watchable source |
766
- | `GET` `DELETE` | `/api/adds` | Adds still in flight — downloads, and local files being hashed — and cancelling a download by URL. A hash cannot be stopped part-way, so it is listed but not cancellable |
767
- | `GET` `POST` | `/api/speed` | Which speed limits are in force, and the manual switch between the two sets |
768
- | `GET` | `/api/categories` | Every category, with the endpoints resolving to its newest build |
769
- | `POST` | `/api/adopt`, `/api/adopt/candidates` | Take over what an engine or another node holds |
770
- | `POST` | `/api/sources/preview` | What a watched web location would take, without taking it |
771
- | `POST` | `/api/subscriptions/preview` | Whether a peer is reachable and what it offers |
772
- | `POST` | `/api/subscriptions/refresh` | Poll subscribed feeds now |
773
- | `POST` | `/api/sources/check` | Check scheduled sources now, rather than at the next due time |
774
- | `POST` | `/api/torrents/:infoHash/hooks/complete` | Run the completion hook again for one archive |
775
- | `GET` `POST` `DELETE` | `/api/tokens`, `/api/tokens/:id` | Mint, list and revoke access tokens |
776
- | `GET` `POST` | `/api/restart` | What a restart would do, and doing it |
777
- | `GET` `DELETE` | `/api/stats` | What this node has served — per-archive counters and the last N requests; `DELETE` clears them |
778
- | `GET` | `/api/traffic` | Upload and download speed per archive over time, from `stats.db`; `infoHash`, `hours` and `buckets` narrow it, and `totals` ranks archives by bytes moved |
779
- | `GET` `PATCH` | `/api/config` | Read and change settings |
780
- | `POST` | `/api/login`, `/api/logout` | Console sign-in |
781
- | `GET` | `/api/session` | Who this request is, and whether a credential is needed at all |
782
- | `GET` | `/api/catalog` | The whole catalogue, for a peer keeping itself in step |
783
- | `GET` | `/archives/:infoHash/tiles.json` | TileJSON — **public** |
784
- | `GET` | `/archives/:infoHash/:z/:x/:y.:ext` | One tile — **public** |
785
- | `GET` | `/health` | 200 when this node can serve, 503 when its engine cannot — **public**, no credential, for a load balancer |
786
- | `GET` | `/archives/:infoHash/ready` | Whether this node can serve _this_ archive yet: 200 ready, 503 not yet, 415 never — **public** |
787
- | `GET` | `/archives/:infoHash/archive.torrent` | The `.torrent` a torrent-aware client joins with — **public** |
788
- | `GET` | `/archives/:infoHash/archive.pmtiles` | The archive itself, by byte range — what any PMTiles reader wants, and a valid web seed. Only where [`serveArchive`](docs/configuration.md#servearchive) is on — **public**. Complete archives, unless [`serveArchiveFromSwarm`](docs/configuration.md#servearchivefromswarm) is set, which answers a bounded range from the swarm instead |
789
- | `GET` | `/archives/:infoHash/preview` | Map preview for one archive — admin, not public |
790
- | `GET` | `/latest/` | Every category and the endpoints that resolve to its newest build — **public**, the index of everything below |
791
- | `GET` | `/latest/:category/tiles.json`, `/:name.torrent`, `/magnet` | The newest build in a category — **public**. The torrent name is yours to choose, so a link can read `planetiler-openmaptiles-latest.torrent`; it redirects to the immutable URL, which names the download after the build it actually is |
792
- | `GET` | `/latest/:category/archive.pmtiles` | The newest build in a category as a file, by byte range — **public**, and gated by [`serveArchive`](docs/configuration.md#servearchive). What to point a PMTiles reader at when you want "whichever is current" rather than one build |
793
- | `GET` | `/feed.xml`, `/feed/:category.xml`, `/latest/:category.xml` | RSS — **public** |
753
+ | Method | Path | Purpose |
754
+ | ---------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
755
+ | `GET` | `/api/status` | Engine health, counts, watched folders, save locations, free space, and whether the swarm can reach this node |
756
+ | `GET` | `/api/torrents` | Catalog joined with live swarm state and what is left of each seeding limit |
757
+ | `POST` | `/api/torrents` | Add via `{path}`, `{url}`, `{magnet}`, `{torrentUrl}`, or a raw `.torrent` body |
758
+ | `DELETE` | `/api/torrents/:infoHash` | Remove (`?deleteData=true` to delete data too) |
759
+ | `GET` | `/api/torrents/:infoHash` | One archive, with disk usage and how it is being read |
760
+ | `GET` | `/api/torrents/:infoHash/file`, `/magnet` | Download the `.torrent`, or its magnet URI |
761
+ | `GET` | `/api/torrents/:infoHash/peers`, `/trackers`, `/content` | Per-peer, per-tracker and per-file detail |
762
+ | `GET` | `/api/torrents/:infoHash/pieces` | Which pieces are held, how rare each is, and what peers hold |
763
+ | `PATCH` | `/api/torrents/:infoHash/mode` | Switch between mirror and cache |
764
+ | `PATCH` | `/api/torrents/:infoHash/categories` | Set, add or remove categories |
765
+ | `PATCH` | `/api/torrents/:infoHash/seeding` | Per-archive seeding limit, or "use the global one" |
766
+ | `PATCH` `GET` | `/api/torrents/:infoHash/location` | Move the data; poll the move |
767
+ | `POST` | `/api/torrents/:infoHash/recheck` | Hash what is on disk again and believe the result — for an archive whose progress and whose files disagree |
768
+ | `POST` | `/api/torrents/:infoHash/pause`, `/resume` | Stop offering it, without forgetting it |
769
+ | `POST` | `/api/torrents/:infoHash/webseeds` | Add web seeds — does not change the infohash |
770
+ | `DELETE` | `/api/torrents/:infoHash/webseeds` | Drop web seeds — likewise |
771
+ | `POST` | `/api/torrents/:infoHash/publish` | What this node offers of the archive itself: `serveArchive`, `selfWebSeed`, `publicDownload` |
772
+ | `POST` | `/api/torrents/:infoHash/warm` | Pre-fetch a region (`GET` for progress, `DELETE` to cancel) |
773
+ | `DELETE` | `/api/torrents/:infoHash/cache` | Reclaim cached pieces, keep the archive |
774
+ | `POST` | `/api/torrents/:infoHash/check` | Has the source changed since the torrent was made? |
775
+ | `POST` | `/api/torrents/:infoHash/rebuild` | Rebuild from the current source (mints a new infohash) |
776
+ | `POST` | `/api/check-origins` | Check every archive with a watchable source |
777
+ | `GET` `DELETE` | `/api/adds` | Adds still in flight — downloads, and local files being hashed — and cancelling a download by URL. A hash cannot be stopped part-way, so it is listed but not cancellable |
778
+ | `GET` `POST` | `/api/speed` | Which speed limits are in force, and the manual switch between the two sets |
779
+ | `GET` | `/api/categories` | Every category, with the endpoints resolving to its newest build |
780
+ | `POST` | `/api/adopt`, `/api/adopt/candidates` | Take over what an engine or another node holds |
781
+ | `POST` | `/api/sources/preview` | What a watched web location would take, without taking it |
782
+ | `POST` | `/api/subscriptions/preview` | Whether a peer is reachable and what it offers |
783
+ | `POST` | `/api/subscriptions/refresh` | Poll subscribed feeds now |
784
+ | `POST` | `/api/sources/check` | Check scheduled sources now, rather than at the next due time |
785
+ | `POST` | `/api/torrents/:infoHash/hooks/complete` | Run the completion hook again for one archive |
786
+ | `GET` `POST` `DELETE` | `/api/tokens`, `/api/tokens/:id` | Mint, list and revoke access tokens |
787
+ | `GET` `POST` | `/api/restart` | What a restart would do, and doing it |
788
+ | `GET` `DELETE` | `/api/stats` | What this node has served — per-archive counters and the last N requests; `DELETE` clears them |
789
+ | `GET` | `/api/traffic` | Upload and download speed per archive over time, from `stats.db`; `infoHash`, `hours` and `buckets` narrow it, and `totals` ranks archives by bytes moved |
790
+ | `GET` `PATCH` | `/api/config` | Read and change settings |
791
+ | `POST` | `/api/login`, `/api/logout` | Console sign-in |
792
+ | `GET` | `/api/session` | Who this request is, and whether a credential is needed at all |
793
+ | `GET` | `/api/catalog` | The whole catalogue, for a peer keeping itself in step |
794
+ | `GET` | `/archives/:infoHash/tiles.json` | TileJSON — **public** |
795
+ | `GET` | `/archives/:infoHash/:z/:x/:y.:ext` | One tile — **public** |
796
+ | `GET` | `/health` | 200 when this node can serve, 503 when its engine cannot — **public**, no credential, for a load balancer |
797
+ | `GET` | `/archives/:infoHash/ready` | Whether this node can serve _this_ archive yet: 200 ready, 503 not yet, 415 never — **public** |
798
+ | `GET` | `/archives/:infoHash/archive.torrent` | The `.torrent` a torrent-aware client joins with — **public** |
799
+ | `GET` | `/archives/:infoHash/archive.pmtiles` | The archive itself, by byte range — what any PMTiles reader wants, and a valid web seed. Only where [`serveArchive`](docs/configuration.md#servearchive) is on — **public**. Complete archives, unless [`serveArchiveFromSwarm`](docs/configuration.md#servearchivefromswarm) is set, which answers a bounded range from the swarm instead |
800
+ | `GET` | `/archives/:infoHash/preview` | Map preview for one archive — admin, not public |
801
+ | `GET` | `/latest/` | Every category and the endpoints that resolve to its newest build — **public**, the index of everything below |
802
+ | `GET` | `/latest/:category/tiles.json`, `/:name.torrent`, `/magnet` | The newest build in a category — **public**. The torrent name is yours to choose, so a link can read `planetiler-openmaptiles-latest.torrent`; it redirects to the immutable URL, which names the download after the build it actually is |
803
+ | `GET` | `/latest/:category/archive.pmtiles` | The newest build in a category as a file, by byte range — **public**, and gated by [`serveArchive`](docs/configuration.md#servearchive). What to point a PMTiles reader at when you want "whichever is current" rather than one build |
804
+ | `GET` | `/api/stacks` | Every stack, with what each source resolved to and why one cannot be served yet |
805
+ | `GET` | `/api/torrents/:infoHash/stacks` | Which stacks would break if this archive were removed, and how |
806
+ | `GET`, `PUT`, `DELETE` | `/api/stacks/:id/raw`, `/api/stacks/:id` | Read a recipe as written, save one, or remove it. `/raw` is the recipe; `/api/stacks` is what it resolved to |
807
+ | `GET` | `/stacks/:id/preview` | A map of a stack, for looking at it — **public** |
808
+ | `GET` | `/stacks/:id/tiles.json` | TileJSON for a stack — **public**. `maxzoom` is the maximum over its sources, not the minimum |
809
+ | `GET` | `/stacks/:id/:size/:z/:x/:y.:ext` | The same tile at 256 or 512 px. A tile's coordinates are an extent rather than a pixel count, so this is the same ground on a finer or coarser grid |
810
+ | `GET` | `/stacks/:id/:z/:x/:y.:ext` | One tile of a stack — **public**. Answered by the topmost source holding it; `X-Stack-Sources` says which were asked and what each said |
811
+ | `GET` | `/feed.xml`, `/feed/:category.xml`, `/latest/:category.xml` | RSS — **public** |
794
812
 
795
813
  Everything under `/api/` is guarded once a credential is configured; tiles, TileJSON and the feeds
796
814
  never are. A `peer` token may read but not change, and may be narrowed to some categories. See
@@ -95,7 +95,7 @@ the question entirely. See [haproxy.md](haproxy.md).
95
95
  | `dataDir` | `'./data'` | catalog, generated `.torrent` files and keys |
96
96
  | `savePath` | unset | where archive data lives |
97
97
  | `cacheSavePath` | unset | separate path for cache-mode pieces |
98
- | `savePathLayout` | `'flat'` | `'flat'` or `'infohash'` |
98
+ | `savePathLayout` | `'flat'` | `'flat'`, `'infohash'` or `'name'` |
99
99
  | `locations` | `[]` | named places for data to land: `[{ name, path }]` |
100
100
  | `incompleteSuffix` | `'.incomplete'` | marker on an archive that is not whole yet |
101
101
  | `completionCheckIntervalSeconds` | `15` | how often to look for finished downloads |
@@ -133,6 +133,8 @@ measurable and clearable as a directory.
133
133
  - `'infohash'` — each archive under `<savePath>/<infohash>/`. Two builds of the
134
134
  same map are both `planet.pmtiles`, and this is the only arrangement in which
135
135
  that can never matter.
136
+ - `'name'` — each archive under `<savePath>/<archive name>/`. The same
137
+ separation, in a directory you can find without knowing the infohash.
136
138
 
137
139
  Flat by default, because the collision it avoids is now refused outright when the
138
140
  second archive is added — so the cost of flat is an error message at the moment
@@ -143,6 +145,24 @@ Only joined archives are placed. One created here keeps the file it was made
143
145
  from, and web seed URLs are built from the published location rather than from
144
146
  the save path.
145
147
 
148
+ Changing this places new arrivals only. Everything already held keeps the save
149
+ path recorded for it, so nothing moves on disk and nothing has to be re-checked.
150
+
151
+ Under `'name'` the directory is settled when the archive is added, from whatever
152
+ name is known then, and never revised afterwards:
153
+
154
+ - The name is taken from the metainfo, or from a magnet's `dn=`. Every magnet
155
+ this node hands out carries one.
156
+ - A **bare magnet** carries no name, so that archive gets its infohash instead.
157
+ The name learned later over BEP 9 does not move it — the data is already
158
+ there, and a rename to tidy the directory is not worth a re-check of it.
159
+ - A name already held by another archive gets the first eight characters of the
160
+ infohash appended, which is the rebuild case: same name, new infohash.
161
+ - The name is written by whoever built the torrent, so it is sanitised down to
162
+ one path segment before it is used. Separators, control characters and the
163
+ ones Windows refuses become `-`; a name that survives none of that falls back
164
+ to the infohash.
165
+
146
166
  ### `incompleteSuffix`
147
167
 
148
168
  Set to an empty string to switch it off, in which case a partial archive is
package/docs/internals.md CHANGED
@@ -103,6 +103,13 @@ writing pieces into one file, and neither ends up with the archive it thinks it
103
103
  has. This is caught when the second archive is added, where it can still be
104
104
  answered by choosing somewhere else to put it.
105
105
 
106
+ `savePathLayout` is the standing answer rather than the per-archive one. Under
107
+ `'infohash'` the collision cannot arise. Under `'name'` it still can — the names
108
+ are what collide — so the second archive takes `<name>-<first eight of the
109
+ infohash>`, and an archive with no usable name at all takes its infohash. Both
110
+ are decided once, when the archive is added; a name learned later over BEP 9
111
+ leaves the directory alone rather than moving the data to match it.
112
+
106
113
  ### Staging, for an archive fetched from a URL
107
114
 
108
115
  An archive fetched over HTTP has no infohash while it is being fetched — the
@@ -868,7 +875,7 @@ carry several categories on purpose — a planet build is both `basemaps` and
868
875
  answer.
869
876
 
870
877
  So the location is chosen rather than derived, and naming them is what makes that
871
- bearable: `M:\_NZB_Finished_Unsorted` is not something anyone should retype, and a
878
+ bearable: `M:\archives\finished-unsorted` is not something anyone should retype, and a
872
879
  name survives the path changing underneath it.
873
880
 
874
881
  Only new data is placed. An archive records where it was put and keeps it, so
@@ -748,6 +748,35 @@ first tile can be seconds away.
748
748
  **Raster archives get the raster.** There is nothing to inspect in an image, so
749
749
  the panel says so and the map is for checking coverage.
750
750
 
751
+ **Terrain archives get hillshade and 3D relief**, and open in it. An archive
752
+ whose [`encoding`](tilejson.md#encoding) is `terrarium`, `mapbox`, or `custom`
753
+ with all four factors present is drawn as a `raster-dem` source: a hillshade
754
+ layer over sea-coloured background, terrain switchable from the control beside
755
+ the compass, and the pitch ceiling raised to 85° — MapLibre's default of 60 is
756
+ not enough to see relief, and looking across a landscape rather than down at it
757
+ is the point of the view.
758
+
759
+ `mlt` is not terrain. It travels in the same `encoding` field and is a vector
760
+ format, which is why the check names the three it can draw rather than testing
761
+ that an encoding is set at all.
762
+
763
+ Terrain and hillshade get a source each over the same URL, which is what
764
+ MapLibre's own terrain example does and what tileserver-gl ships. The two ask a
765
+ DEM source for different things — one is sampled for height across the whole
766
+ viewport, the other is shaded per tile — and sharing one between them has a
767
+ history of rendering artefacts. The tiles are requested once either way, since
768
+ the HTTP cache answers the second source.
769
+
770
+ **The raw tiles stay one click away**, from the link in the header, and the map
771
+ position survives the switch. That view is not decorative: a missing DEM tile
772
+ hillshades as flat ground rather than as missing, so the only way to see a hole
773
+ is to look at the pixels. A `custom` archive carrying none of its four factors
774
+ is drawn as an ordinary raster with a note saying why — pixels whose meaning is
775
+ not stated cannot honestly be rendered as heights.
776
+
777
+ Stacks preview through the same page, so a terrain stack gets the terrain view
778
+ from its own declared output encoding. See [tile stacks](tile-stacks.md).
779
+
751
780
  **What it has actually served** is on the archive's detail, as `served`, and
752
781
  across the node at `GET /api/stats` — requests, bytes, a breakdown by zoom and
753
782
  status, and which client addresses asked. Worth reading beside `reading`: an