pmtiles-swarm 0.41.1 → 0.41.2
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 +25 -0
- package/package.json +1 -1
- package/src/library.js +78 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.41.2
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
|
|
13
|
+
### 🐞 Bug fixes
|
|
14
|
+
- **An archive that finished downloading during an unclean stop no longer comes back at 0% for ever.**
|
|
15
|
+
A downloaded archive is written under a marker — `planet.pmtiles.incomplete` — and renamed the
|
|
16
|
+
instant it is whole. The rename and the catalog entry recording it are two steps, so a stop between
|
|
17
|
+
them leaves an archive that is finished on disk and unfinished in the record.
|
|
18
|
+
|
|
19
|
+
That disagreement was permanent, and worse than it looks. Restore re-added the entry with the marker
|
|
20
|
+
attached, so the engine opened a filename nothing was at any more, found no data, and began
|
|
21
|
+
downloading an archive the node already held. The sweep that would have noticed takes the engine's
|
|
22
|
+
word over the disk's whenever the engine has one — and the engine's word was now 0%. Rechecking did
|
|
23
|
+
not help either, since it hashes the marked name, which is the wrong file to look at. No number of
|
|
24
|
+
restarts recovered it.
|
|
25
|
+
|
|
26
|
+
Restore now checks the disk before handing over anything recorded as unfinished: marked file gone
|
|
27
|
+
and the real one whole means the rename happened and the record of it did not, so the entry is
|
|
28
|
+
corrected and the archive handed over as the complete thing it is. A genuine partial download is
|
|
29
|
+
left exactly as it was — claiming otherwise would offer peers an archive this node cannot serve.
|
|
30
|
+
|
|
31
|
+
This is why the archives that arrive from a feed or a URL were the ones that sat at 0%: an archive
|
|
32
|
+
built here is recorded complete the moment it is registered, having just been read end to end, so it
|
|
33
|
+
never carries a marker to disagree about.
|
|
34
|
+
|
|
10
35
|
## 0.41.1
|
|
11
36
|
### ✨ Features and improvements
|
|
12
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.2",
|
|
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/library.js
CHANGED
|
@@ -1819,6 +1819,72 @@ export class Library {
|
|
|
1819
1819
|
return queued;
|
|
1820
1820
|
}
|
|
1821
1821
|
|
|
1822
|
+
/**
|
|
1823
|
+
* Reconciles what the catalog says about an archive with what is on disk.
|
|
1824
|
+
*
|
|
1825
|
+
* An archive downloaded here is written under a marker — `planet.pmtiles`
|
|
1826
|
+
* becomes `planet.pmtiles.incomplete` — and renamed the instant it is whole.
|
|
1827
|
+
* The rename and the catalog entry that records it are two steps, so a stop
|
|
1828
|
+
* between them leaves an archive that is finished on disk and unfinished in
|
|
1829
|
+
* the record.
|
|
1830
|
+
*
|
|
1831
|
+
* That disagreement does not settle itself, and it is worse than it looks.
|
|
1832
|
+
* Restore re-adds an entry it believes incomplete with the marker attached,
|
|
1833
|
+
* so the engine opens `planet.pmtiles.incomplete` — a name nothing is at any
|
|
1834
|
+
* more — finds no data, and begins downloading a 128 GiB archive this node
|
|
1835
|
+
* already holds. The sweep that would notice takes the engine's word over
|
|
1836
|
+
* the disk's whenever the engine has one, and the engine's word is now 0%.
|
|
1837
|
+
* Rechecking does not help either: it hashes the marked name, which is the
|
|
1838
|
+
* wrong file to be looking at. Nothing recovers, through any number of
|
|
1839
|
+
* restarts.
|
|
1840
|
+
*
|
|
1841
|
+
* Only archives that were downloaded can be in this state — one built here
|
|
1842
|
+
* is recorded complete the moment it is registered, having just been read
|
|
1843
|
+
* end to end — which is why the archives that come from a feed or a URL are
|
|
1844
|
+
* the ones that sit at 0%.
|
|
1845
|
+
* @param {object} entry - The catalog entry restore is about to hand over.
|
|
1846
|
+
* @returns {Promise<object>} - That entry, or the corrected one.
|
|
1847
|
+
*/
|
|
1848
|
+
async #reconcileMarker(entry) {
|
|
1849
|
+
if (entry.complete !== false || !entry.savePath || !entry.name) {
|
|
1850
|
+
return entry;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
const marked = onDiskPath(entry, this.#config);
|
|
1854
|
+
const real = path.join(entry.savePath, entry.name);
|
|
1855
|
+
// Markers turned off, so there is no second name to disagree with.
|
|
1856
|
+
if (!marked || marked === real) return entry;
|
|
1857
|
+
|
|
1858
|
+
// Still under the marker, which is what an unfinished download should look
|
|
1859
|
+
// like. Nothing to correct, whatever its progress.
|
|
1860
|
+
const partial = await fs.stat(marked).then(
|
|
1861
|
+
() => true,
|
|
1862
|
+
() => false,
|
|
1863
|
+
);
|
|
1864
|
+
if (partial) return entry;
|
|
1865
|
+
|
|
1866
|
+
// The marked name is gone and the real one is whole: the rename happened
|
|
1867
|
+
// and the record of it did not.
|
|
1868
|
+
const whole = await alreadyComplete({
|
|
1869
|
+
savePath: entry.savePath,
|
|
1870
|
+
name: entry.name,
|
|
1871
|
+
size: entry.size,
|
|
1872
|
+
});
|
|
1873
|
+
if (!whole) return entry;
|
|
1874
|
+
|
|
1875
|
+
console.warn(
|
|
1876
|
+
`[complete] ${entry.name} is whole under its own name but was recorded ` +
|
|
1877
|
+
'as unfinished — a stop between the rename and the record. Handing it ' +
|
|
1878
|
+
'to the engine as the complete archive it is.',
|
|
1879
|
+
);
|
|
1880
|
+
return (
|
|
1881
|
+
(await this.#catalog.put({
|
|
1882
|
+
infoHash: entry.infoHash,
|
|
1883
|
+
complete: true,
|
|
1884
|
+
})) ?? { ...entry, complete: true }
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1822
1888
|
/**
|
|
1823
1889
|
* Checks that what restore just claimed is actually being seeded.
|
|
1824
1890
|
*
|
|
@@ -1960,8 +2026,9 @@ export class Library {
|
|
|
1960
2026
|
}, 15_000);
|
|
1961
2027
|
progress.unref?.();
|
|
1962
2028
|
|
|
2029
|
+
const handed = [];
|
|
1963
2030
|
try {
|
|
1964
|
-
await this.#restoreEach(entries, tally);
|
|
2031
|
+
await this.#restoreEach(entries, tally, handed);
|
|
1965
2032
|
} finally {
|
|
1966
2033
|
clearInterval(progress);
|
|
1967
2034
|
}
|
|
@@ -1971,7 +2038,7 @@ export class Library {
|
|
|
1971
2038
|
// reports on a library half restored. Never allowed to fail the restore —
|
|
1972
2039
|
// this is a report about seeding, and a node that could not produce it is
|
|
1973
2040
|
// still a node that restored what it could.
|
|
1974
|
-
await this.#verifySeeding(
|
|
2041
|
+
await this.#verifySeeding(handed).catch((error) =>
|
|
1975
2042
|
console.warn(
|
|
1976
2043
|
`[seeding] could not check what is being seeded: ${error.message}`,
|
|
1977
2044
|
),
|
|
@@ -1989,7 +2056,7 @@ export class Library {
|
|
|
1989
2056
|
* @param {{restored: number, failed: number}} tally - Mutated as it goes.
|
|
1990
2057
|
* @returns {Promise<{restored: number, failed: number}>} - That tally.
|
|
1991
2058
|
*/
|
|
1992
|
-
async #restoreEach(entries, tally) {
|
|
2059
|
+
async #restoreEach(entries, tally, handed) {
|
|
1993
2060
|
for (const entry of entries) {
|
|
1994
2061
|
// An engine that cannot open its port will fail every one of these, each
|
|
1995
2062
|
// after its own timeout. Stopping at the first is the difference between
|
|
@@ -2024,11 +2091,18 @@ export class Library {
|
|
|
2024
2091
|
}
|
|
2025
2092
|
}
|
|
2026
2093
|
|
|
2094
|
+
// Before the add, because the add is what acts on the disagreement:
|
|
2095
|
+
// an entry wrongly recorded as unfinished is handed to the engine
|
|
2096
|
+
// under a filename nothing is at, and the engine starts downloading
|
|
2097
|
+
// an archive that is already here.
|
|
2098
|
+
const settled = await this.#reconcileMarker(entry);
|
|
2099
|
+
|
|
2027
2100
|
// Through the same path as every other re-add, so an archive stored
|
|
2028
2101
|
// with no trackers is repaired here too. Restoring used to build its
|
|
2029
2102
|
// own add and skip that, which is why an archive that could not find a
|
|
2030
2103
|
// peer stayed unable to find one across every restart.
|
|
2031
|
-
await this.#readd(
|
|
2104
|
+
await this.#readd(settled);
|
|
2105
|
+
handed.push(settled);
|
|
2032
2106
|
tally.restored++;
|
|
2033
2107
|
} catch (error) {
|
|
2034
2108
|
tally.failed++;
|