@inoku/lepan 0.7.64 → 0.7.66
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/README.md +29 -3
- package/USER_MANUAL.md +4 -2
- package/bin/lifedrive.js +58 -16
- package/dist/lifedrive-bundle.tar.gz +2 -2
- package/dist/lifedrive-bundle.tar.gz.sha256 +1 -1
- package/dist/lifedrive-install.ps1 +3 -3
- package/dist/lifedrive-install.sh +178 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,9 +30,11 @@ To upgrade an existing key-auth installation without changing its authorized dev
|
|
|
30
30
|
npx --yes @inoku/lepan@latest --upgrade
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The installer reuses a running Leither node and finds its directory automatically. If none is running, Linux/macOS fresh setup first looks for Leither in the selected directory, the current directory, or on `PATH`; otherwise it uses `~/.local/share/
|
|
33
|
+
The installer reuses a running Leither node and finds its directory automatically. If none is running, Linux/macOS fresh setup first looks for Leither in the selected directory, the current directory, or on `PATH`; otherwise it uses `~/.local/share/leither`. Windows uses `%ProgramData%\LifeDrive\Leither` unless `--leither-root` selects another directory. It downloads the matching binary from the [official Leither distribution](http://vzhan.cn/#start.html), verifies its SHA-256 checksum, initializes local configuration and keys, installs boot startup, and waits for the managed process and local version endpoint before continuing. Existing executables, configuration, and keys are never replaced.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Fresh Linux/macOS installations keep LePan application files in `<Leither root>/lepan` (by default `~/.local/share/leither/lepan`). This also applies to the macOS setup package. The folder contains `lifeDrive/` and `lifedrive-identity/`; compatibility links in the node root preserve the published app identity and service paths. Private node and household state stays in the node root. Existing installations retain their directories during upgrades.
|
|
36
|
+
|
|
37
|
+
When the installer creates a new node, it shows the free space on the selected drive and asks how many whole decimal gigabytes Leither may use. The default is **100 GB**. This is a node-wide maximum covering LePan and every other application stored by that Leither node; it does not reserve or preallocate the space. The installer refuses a value larger than the space currently available and retains the choice before Leither first starts. On Linux/macOS, the first start creates the node key and datastore configuration; setup then applies the limit and restarts only that newly created, managed node before continuing. If setup is interrupted, rerunning fresh setup finishes the pending storage configuration. For unattended setup, pass `--storage-max-gb GB` or set `LIFEDRIVE_STORAGE_MAX_GB`. Existing nodes and upgrades keep their current limit; change those from LePan Settings.
|
|
36
38
|
|
|
37
39
|
Choose another empty directory for a fresh node, select a stopped node, or select among multiple running instances:
|
|
38
40
|
|
|
@@ -84,7 +86,31 @@ The Linux/macOS legacy browser-owner setup uses the built-in AV1 registration en
|
|
|
84
86
|
|
|
85
87
|
LifeDrive requires Leither V0.24.11 or newer. Linux/macOS browser-owner setup creates a separate Ed25519 `sodiumv2` identity for the primary browser. Windows prints a private ten-minute invitation for the first paired phone; later users and devices are managed from a paired device.
|
|
86
88
|
|
|
87
|
-
After successful npm setup, the installer saves a private `lepan-pairing-<id>.png` and prints its full path. It also displays the QR code when the terminal
|
|
89
|
+
After successful npm setup, the installer saves a private `lepan-pairing-<id>.png` and prints its full path. It also displays the QR code when it fits the terminal's width and height. In the iPhone or Android app, open **Settings → Set up users → Scan QR code** and scan the terminal or the saved image. A private `.json` invitation is saved alongside it for apps without a setup scanner: transfer that file to the phone and use **Choose identity file**. These files are saved in the Leither root on Linux/macOS and its private `.lifedrive-household` directory on Windows. Household invitations expire ten minutes after creation; legacy identity codes contain a device private key, so keep them private and delete the transfer copies after pairing. Legacy `--add-device` and `--upgrade` also produce a code when an identity is available; household upgrades preserve existing pairings without issuing new invitations.
|
|
90
|
+
|
|
91
|
+
### Renew an expired first-phone invitation
|
|
92
|
+
|
|
93
|
+
Run this again **on the computer running Leither**:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx --yes @inoku/lepan@latest --household
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If more than one node is running, also pass `--leither-root` with the same node
|
|
100
|
+
directory used during installation. On Windows, use an administrator terminal.
|
|
101
|
+
The macOS setup package also supports renewal when run again.
|
|
102
|
+
|
|
103
|
+
Before the first owner has been created, setup replaces the previous invitation
|
|
104
|
+
and produces a fresh QR image and import file with a new ten-minute expiry. It
|
|
105
|
+
briefly stops only the managed account service while issuing the invitation,
|
|
106
|
+
then starts it and verifies readiness before displaying the new code. If issuance
|
|
107
|
+
fails, it attempts to restore the service. Leither keeps running; existing node
|
|
108
|
+
keys and drive data are preserved.
|
|
109
|
+
|
|
110
|
+
Setup never replaces an existing owner or an interrupted account claim. If users
|
|
111
|
+
already exist, use **My devices** on a paired phone to invite another device. An
|
|
112
|
+
interrupted first claim must be resumed on the original phone with its original
|
|
113
|
+
invitation. Ordinary `--upgrade` does not renew invitations.
|
|
88
114
|
|
|
89
115
|
An existing installation is backed up before replacement. The explicit `--upgrade` path requires the saved application and key-auth owner state before changing files, synchronizes the same published application MID, and does not rerun device or domain setup. This release intentionally does not migrate the earlier password-owner prototype. Application files are active only after Leither advances the application from `cur` to `last`; a publication timeout leaves the prior `last` version serving users.
|
|
90
116
|
|
package/USER_MANUAL.md
CHANGED
|
@@ -24,7 +24,7 @@ A LifeDrive node needs:
|
|
|
24
24
|
| A startup manager | systemd on Linux, launchd on macOS, or Task Scheduler on Windows starts the node at boot and recovers it after crashes. |
|
|
25
25
|
| The LifeDrive app on your phone | iPhone or Android. The first user is created from a phone. |
|
|
26
26
|
|
|
27
|
-
LifeDrive never
|
|
27
|
+
Normal LifeDrive upgrades never stop or restart Leither. Linux/macOS fresh setup restarts only the node it just created to activate the selected storage limit. If more than one Leither node runs on
|
|
28
28
|
the machine, the installer lists them and asks you to choose one with
|
|
29
29
|
`--leither-root <directory>`.
|
|
30
30
|
|
|
@@ -53,7 +53,7 @@ The same command also remains available from an Administrator Windows Terminal f
|
|
|
53
53
|
The installer finds an existing running Leither node. If none is running, it
|
|
54
54
|
reuses Leither in the selected directory, current directory, or `PATH`, or
|
|
55
55
|
downloads and verifies the official runtime for your machine. A new node is
|
|
56
|
-
created in `~/.local/share/
|
|
56
|
+
created in `~/.local/share/leither` on Linux/macOS or
|
|
57
57
|
`%ProgramData%\LifeDrive\Leither` on Windows, initialized with its own private
|
|
58
58
|
keys, and prepared for automatic boot startup. Before starting the service,
|
|
59
59
|
setup shows the selected drive's available space and asks for the maximum space
|
|
@@ -64,6 +64,8 @@ service, waits for Leither's local version
|
|
|
64
64
|
endpoint, verifies the LifeDrive archive, installs LifeDrive and runs terminal
|
|
65
65
|
setup. Follow its prompts.
|
|
66
66
|
|
|
67
|
+
Fresh Linux/macOS installations keep LePan application files in `<Leither root>/lepan` (by default `~/.local/share/leither/lepan`). This also applies to the macOS setup package. The folder contains `lifeDrive/` and `lifedrive-identity/`; compatibility links in the node root preserve the published app identity and service paths. Private node and household state stays in the node root. Existing installations retain their directories during upgrades.
|
|
68
|
+
|
|
67
69
|
To choose another empty directory or start a stopped existing node:
|
|
68
70
|
|
|
69
71
|
```bash
|
package/bin/lifedrive.js
CHANGED
|
@@ -22,24 +22,59 @@ async function showPairingCode(resultPath) {
|
|
|
22
22
|
}
|
|
23
23
|
const directory = fs.readFileSync(`${resultPath}.directory`, "utf8");
|
|
24
24
|
if (!path.isAbsolute(directory)) throw new Error("Invalid pairing image directory");
|
|
25
|
-
const QRCode = require("qrcode");
|
|
26
25
|
const payload = JSON.stringify(pairing);
|
|
26
|
+
// Only encode fields consumed by the existing Android and iOS setup parsers.
|
|
27
|
+
// The node enforces expiry and returns account details after a successful claim.
|
|
28
|
+
// Keep the full invitation in the import file, and leave legacy identities intact.
|
|
29
|
+
// iOS still requires certificate_sha256 even when Leither transport uses a key.
|
|
30
|
+
const qrPayload = pairing.type === "lifedrive-node-setup"
|
|
31
|
+
? JSON.stringify(Object.fromEntries([
|
|
32
|
+
"type", "version", "app_id", "node_id", "endpoint", "transport",
|
|
33
|
+
"transport_key", "certificate_sha256", "request_id", "secret",
|
|
34
|
+
].filter(key => Object.hasOwn(pairing, key)).map(key => [key, pairing[key]])))
|
|
35
|
+
: payload;
|
|
36
|
+
const basename = path.join(directory, `lepan-pairing-${randomUUID()}`);
|
|
37
|
+
const imagePath = `${basename}.png`;
|
|
38
|
+
const identityPath = `${basename}.json`;
|
|
39
|
+
// Older mobile releases support importing this file but have no setup scanner.
|
|
40
|
+
// Keep a private copy even when QR generation succeeds.
|
|
41
|
+
fs.writeFileSync(identityPath, payload, { flag: "wx", mode: 0o600 });
|
|
27
42
|
const options = { errorCorrectionLevel: "M", margin: 4, scale: 8 };
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
const privacyNote = pairing.type === "lifedrive-node-setup"
|
|
44
|
+
? "Keep it private. This invitation expires ten minutes after creation."
|
|
45
|
+
: "This contains your device private key. Keep it private and delete it after pairing.";
|
|
46
|
+
const renewalHint = pairing.type === "lifedrive-node-setup"
|
|
47
|
+
? "\nInvitation expired? Run this again on the node:\n npx --yes @inoku/lepan@latest --household\n"
|
|
48
|
+
: "";
|
|
49
|
+
let modules;
|
|
50
|
+
try {
|
|
51
|
+
const QRCode = require("qrcode");
|
|
52
|
+
const png = await QRCode.toBuffer(qrPayload, options);
|
|
53
|
+
modules = QRCode.create(qrPayload, options).modules;
|
|
54
|
+
// Unique names and exclusive creation avoid overwriting another device's
|
|
55
|
+
// code or following a pre-existing symlink. Keep this outside the app bundle.
|
|
56
|
+
fs.writeFileSync(imagePath, png, { flag: "wx", mode: 0o600 });
|
|
57
|
+
} catch {
|
|
58
|
+
// The child no longer prints secrets. Preserve an importable fallback
|
|
59
|
+
// before runInstaller removes the temporary handoff directory.
|
|
60
|
+
process.stderr.write("\nNext: pair your phone\n\nThe QR code could not be created. Use the saved pairing file instead.\n");
|
|
61
|
+
process.stderr.write(` ${identityPath}\n\n`);
|
|
62
|
+
process.stderr.write("Transfer this file to your phone, then select it in LePan > Settings > Set up users > Choose identity file.\n");
|
|
63
|
+
process.stderr.write(`${privacyNote}\n`);
|
|
64
|
+
process.stderr.write(renewalHint);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
process.stdout.write("\nNext: pair your phone\n\n");
|
|
68
|
+
process.stdout.write(" 1. Open LePan on your iPhone or Android phone.\n");
|
|
69
|
+
process.stdout.write(" 2. Go to Settings > Set up users > Scan QR code.\n");
|
|
38
70
|
// Small terminal codes use half-height blocks. If the terminal would wrap,
|
|
39
|
-
//
|
|
40
|
-
const modules = QRCode.create(payload, options).modules;
|
|
71
|
+
// or scroll the instructions out of view, use the full-resolution PNG.
|
|
41
72
|
const width = modules.size + options.margin * 2;
|
|
42
|
-
|
|
73
|
+
const height = Math.ceil(width / 2);
|
|
74
|
+
const fitsTerminal = process.stdout.isTTY && process.stdout.columns > width &&
|
|
75
|
+
process.stdout.rows >= height + 26;
|
|
76
|
+
if (fitsTerminal) {
|
|
77
|
+
process.stdout.write(" 3. Scan the code below.\n\n");
|
|
43
78
|
// Explicit colors and a four-module quiet zone work on light/dark themes.
|
|
44
79
|
const dark = (x, y) => x >= 0 && y >= 0 && x < modules.size && y < modules.size && modules.get(y, x);
|
|
45
80
|
let terminal = "";
|
|
@@ -52,8 +87,15 @@ async function showPairingCode(resultPath) {
|
|
|
52
87
|
}
|
|
53
88
|
process.stdout.write(terminal);
|
|
54
89
|
} else {
|
|
55
|
-
process.stdout.write("Open the saved
|
|
90
|
+
process.stdout.write(" 3. Open the saved QR image on this computer and scan it.\n");
|
|
56
91
|
}
|
|
92
|
+
process.stdout.write(`\nQR image:\n ${imagePath}\n\n${privacyNote}\n`);
|
|
93
|
+
process.stdout.write("\nNo Scan QR code button? Transfer this pairing file to your phone:\n");
|
|
94
|
+
process.stdout.write(` ${identityPath}\n`);
|
|
95
|
+
process.stdout.write("Then open Settings > Set up users > Choose identity file.\n");
|
|
96
|
+
process.stdout.write("Keep the pairing file private; delete the transfer copy after import.\n");
|
|
97
|
+
process.stdout.write("Keep this computer on and connected while using LePan.\n");
|
|
98
|
+
process.stdout.write(renewalHint);
|
|
57
99
|
}
|
|
58
100
|
|
|
59
101
|
async function runInstaller(command, args) {
|
|
@@ -73,7 +115,7 @@ async function runInstaller(command, args) {
|
|
|
73
115
|
try { await showPairingCode(resultPath); }
|
|
74
116
|
catch {
|
|
75
117
|
// Parser errors may contain identity text; never print them.
|
|
76
|
-
process.stderr.write("
|
|
118
|
+
process.stderr.write("\nPhone pairing could not be prepared. Use your existing identity file, or create a new setup invitation from the node.\n");
|
|
77
119
|
}
|
|
78
120
|
}
|
|
79
121
|
} finally {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/dist/lifedrive-bundle.tar.gz
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 44139710 bytes
|
|
4
|
+
sha256: 134de0bb85330eb3192e837e5054074969a80cbb4eaa1ee0b7870e55b9a3b310
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
134de0bb85330eb3192e837e5054074969a80cbb4eaa1ee0b7870e55b9a3b310 lifedrive-bundle.tar.gz
|
|
@@ -27,7 +27,7 @@ Windows options:
|
|
|
27
27
|
--no-install-leither Require an already running Leither node.
|
|
28
28
|
--leither-service Configure Leither startup only.
|
|
29
29
|
--upgrade Upgrade an existing Windows LePan installation.
|
|
30
|
-
--household
|
|
30
|
+
--household Set up household users or renew an unclaimed setup invitation.
|
|
31
31
|
|
|
32
32
|
Run Windows Terminal or PowerShell as Administrator. Windows setup supports
|
|
33
33
|
native x64 Windows 10/11 and Windows Server; WSL and Git Bash are not required.
|
|
@@ -382,8 +382,8 @@ if ($Upgrade) {
|
|
|
382
382
|
if (Test-Path -LiteralPath (Join-Path $root "lifeDrive.owner") -PathType Leaf) {
|
|
383
383
|
throw "This node uses the legacy browser-owner setup, which Windows does not migrate automatically. Its files were left unchanged."
|
|
384
384
|
}
|
|
385
|
-
if (Test-Path -LiteralPath $routes -PathType Leaf) {
|
|
386
|
-
throw "
|
|
385
|
+
if ((Test-Path -LiteralPath $routes -PathType Leaf) -and -not $Household) {
|
|
386
|
+
throw "A LePan household already exists. Use --household for phone setup, or --upgrade for application updates."
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
|
|
@@ -28,7 +28,7 @@ Installer options:
|
|
|
28
28
|
--storage-max-gb GB Maximum disk space for a newly installed Leither node (default: 100 GB).
|
|
29
29
|
--release-base URL Alternate GitHub Release asset base URL.
|
|
30
30
|
--upgrade Upgrade an existing LePan without changing device authorization.
|
|
31
|
-
--household
|
|
31
|
+
--household Set up household users or renew an unclaimed setup invitation.
|
|
32
32
|
--household-config FILE Install mobile-led household setup using a private service configuration.
|
|
33
33
|
|
|
34
34
|
Setup options are forwarded to lifeDrive-setup.sh. Common examples:
|
|
@@ -164,22 +164,22 @@ available_storage_gb() {
|
|
|
164
164
|
LC_ALL=C df -Pk "$LEITHER_WORKDIR" 2>/dev/null | awk 'NR == 2 && $4 ~ /^[0-9]+$/ { print int(($4 * 1024) / 1000000000) }'
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
local available_gb selected_gb default_gb=100
|
|
169
|
-
|
|
170
|
-
[[ -
|
|
171
|
-
|
|
172
|
-
return 1
|
|
173
|
-
|
|
167
|
+
choose_new_node_storage() {
|
|
168
|
+
local available_gb selected_gb default_gb=100
|
|
169
|
+
local pending="$LEITHER_WORKDIR/.lifedrive-storage-pending"
|
|
170
|
+
if [[ -z "$STORAGE_MAX_GB" && -s "$pending" ]]; then
|
|
171
|
+
STORAGE_MAX_GB=$(cat "$pending")
|
|
172
|
+
valid_storage_gb "$STORAGE_MAX_GB" || { echo "Invalid saved Leither storage choice." >&2; return 1; }
|
|
173
|
+
fi
|
|
174
174
|
available_gb=$(available_storage_gb)
|
|
175
|
-
|
|
176
|
-
echo "
|
|
175
|
+
printf '\nStorage\n'
|
|
176
|
+
echo "Set the disk space limit for this node (LePan and other Leither data)."
|
|
177
177
|
if [[ "$available_gb" =~ ^[0-9]+$ ]]; then
|
|
178
|
-
echo "Available
|
|
178
|
+
echo " Available: ${available_gb} GB"
|
|
179
179
|
fi
|
|
180
180
|
if [[ -n "$STORAGE_MAX_GB" ]]; then
|
|
181
181
|
selected_gb="$STORAGE_MAX_GB"
|
|
182
|
-
echo "
|
|
182
|
+
echo " Selected limit: ${selected_gb} GB"
|
|
183
183
|
elif [[ -t 0 ]]; then
|
|
184
184
|
while true; do
|
|
185
185
|
read -r -p "Maximum Leither storage in GB [$default_gb]: " selected_gb
|
|
@@ -203,7 +203,18 @@ configure_new_node_storage() {
|
|
|
203
203
|
echo "Free space or rerun setup with --storage-max-gb set to a smaller value." >&2
|
|
204
204
|
return 1
|
|
205
205
|
fi
|
|
206
|
-
|
|
206
|
+
STORAGE_MAX_GB="$selected_gb"
|
|
207
|
+
# Retain the choice if service startup fails or the installer is interrupted.
|
|
208
|
+
(umask 077; printf '%s\n' "$selected_gb" > "$pending")
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
configure_new_node_storage() {
|
|
212
|
+
local config="$LEITHER_WORKDIR/ds/config"
|
|
213
|
+
[[ -f "$config" && ! -L "$config" ]] || {
|
|
214
|
+
echo "Leither startup did not create a regular datastore configuration at $config." >&2
|
|
215
|
+
return 1
|
|
216
|
+
}
|
|
217
|
+
node - "$config" "${STORAGE_MAX_GB}GB" <<'NODE' || return 1
|
|
207
218
|
const fs = require("node:fs"), path = require("node:path");
|
|
208
219
|
const file = process.argv[2], desired = process.argv[3];
|
|
209
220
|
const info = fs.lstatSync(file);
|
|
@@ -241,7 +252,6 @@ try {
|
|
|
241
252
|
throw error;
|
|
242
253
|
}
|
|
243
254
|
NODE
|
|
244
|
-
echo "Leither storage limit saved as ${selected_gb} GB."
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
install_leither_binary() (
|
|
@@ -464,7 +474,8 @@ configure_macos_leither_firewall() {
|
|
|
464
474
|
return 1
|
|
465
475
|
}
|
|
466
476
|
|
|
467
|
-
|
|
477
|
+
printf '\nNetwork access\n'
|
|
478
|
+
echo "Allowing connections through the macOS firewall..."
|
|
468
479
|
firewall_apps=$("$firewall" --listapps) || {
|
|
469
480
|
echo "LePan setup could not read the macOS Application Firewall rules." >&2
|
|
470
481
|
return 1
|
|
@@ -490,7 +501,7 @@ configure_macos_leither_firewall() {
|
|
|
490
501
|
echo "The macOS Application Firewall still reports Leither as blocked." >&2
|
|
491
502
|
return 1
|
|
492
503
|
}
|
|
493
|
-
echo "
|
|
504
|
+
echo " Firewall access ready."
|
|
494
505
|
|
|
495
506
|
block_all_state=$("$firewall" --getblockall 2>/dev/null || true)
|
|
496
507
|
if [[ "$block_all_state" == *"enabled"* ]]; then
|
|
@@ -499,6 +510,77 @@ configure_macos_leither_firewall() {
|
|
|
499
510
|
fi
|
|
500
511
|
}
|
|
501
512
|
|
|
513
|
+
wait_for_leither() {
|
|
514
|
+
local version="$1" port="$2" previous_pid="${3:-}" attempt response current_pid
|
|
515
|
+
for attempt in {1..30}; do
|
|
516
|
+
current_pid=$(leither_service_pid || true)
|
|
517
|
+
response=$(curl --noproxy '*' -fsS --max-time 2 "http://127.0.0.1:$port/getvar?name=ver" 2>/dev/null || true)
|
|
518
|
+
if [[ -z "$previous_pid" || "$current_pid" != "$previous_pid" ]] &&
|
|
519
|
+
leither_service_owns_process &&
|
|
520
|
+
node -e 'try { process.exit(JSON.parse(process.argv[1]).replace(/^V/, "") === process.argv[2] ? 0 : 1); } catch { process.exit(1); }' "$response" "$version"; then
|
|
521
|
+
return 0
|
|
522
|
+
fi
|
|
523
|
+
sleep 1
|
|
524
|
+
done
|
|
525
|
+
echo "Leither did not become ready. Its files and service were retained." >&2
|
|
526
|
+
echo "Linux: journalctl -u lifedrive-leither.service. macOS: $LEITHER_WORKDIR/leither-service.log." >&2
|
|
527
|
+
return 1
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
verify_new_node_storage() {
|
|
531
|
+
local report
|
|
532
|
+
report=$(cd "$LEITHER_WORKDIR" && ./Leither repo stat --json 2>/dev/null) || return 1
|
|
533
|
+
# Some Leither versions still return a text table with --json. Read only the
|
|
534
|
+
# numeric active limit; never print the complete node response.
|
|
535
|
+
printf '%s' "$report" | node -e '
|
|
536
|
+
const fs = require("node:fs");
|
|
537
|
+
try {
|
|
538
|
+
const source = fs.readFileSync(0, "utf8");
|
|
539
|
+
let value;
|
|
540
|
+
try {
|
|
541
|
+
const reply = JSON.parse(source), data = reply.data ?? reply;
|
|
542
|
+
value = data.StorageMax ?? data.storageMax;
|
|
543
|
+
} catch {
|
|
544
|
+
value = source.match(/^StorageMax:[ \t]*(\d+)[ \t]*$/m)?.[1];
|
|
545
|
+
}
|
|
546
|
+
if (value === undefined || BigInt(value) !== BigInt(process.argv[1]) * 1000000000n) process.exit(1);
|
|
547
|
+
} catch { process.exit(1); }
|
|
548
|
+
' "$STORAGE_MAX_GB"
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
finish_new_node_storage() {
|
|
552
|
+
local pending="$LEITHER_WORKDIR/.lifedrive-storage-pending" previous_pid version port
|
|
553
|
+
[[ -f "$pending" && ! -L "$pending" ]] || return 1
|
|
554
|
+
# A restart is permitted only for the new node this installer is still
|
|
555
|
+
# provisioning, never a normal upgrade or another manager's process.
|
|
556
|
+
(( ! UPGRADE_ONLY && ! MANAGEMENT_ONLY && INSTALL_LEITHER )) || {
|
|
557
|
+
echo "Finish the pending fresh installation before upgrading or managing devices." >&2; return 1;
|
|
558
|
+
}
|
|
559
|
+
[[ ! -s "$LEITHER_WORKDIR/lifeDrive.owner" && ! -s "$LEITHER_WORKDIR/lifeDrive.households.json" ]] || {
|
|
560
|
+
echo "Storage setup marker conflicts with an initialized LePan installation." >&2; return 1;
|
|
561
|
+
}
|
|
562
|
+
leither_service_owns_process || { echo "Pending storage setup belongs to a node outside the managed service." >&2; return 1; }
|
|
563
|
+
[[ -s "$LEITHER_WORKDIR/hostkey.cfg" ]] || { echo "Leither startup did not create its node key." >&2; return 1; }
|
|
564
|
+
choose_new_node_storage || return 1
|
|
565
|
+
configure_new_node_storage || return 1
|
|
566
|
+
version=$(leither_version "$LEITHER_WORKDIR") || return 1
|
|
567
|
+
port=$(node -e 'const fs=require("node:fs"),p=require("node:path"),r=process.argv[1];const f=["SystemVars.json","systemvars.json"].map(n=>p.join(r,n)).find(f=>fs.existsSync(f));const v=JSON.parse(fs.readFileSync(f)).ServicePort??4800;if(!Number.isInteger(v)||v<1||v>65535)process.exit(1);process.stdout.write(String(v));' "$LEITHER_WORKDIR") || return 1
|
|
568
|
+
previous_pid=$(leither_service_pid) || return 1
|
|
569
|
+
echo "Applying the storage limit (restarting the newly created node)..."
|
|
570
|
+
case "$(uname -s)" in
|
|
571
|
+
Linux) sudo systemctl restart lifedrive-leither.service || return 1 ;;
|
|
572
|
+
Darwin) sudo launchctl kickstart -k system/uk.inoku.leither || return 1 ;;
|
|
573
|
+
*) return 1 ;;
|
|
574
|
+
esac
|
|
575
|
+
wait_for_leither "$version" "$port" "$previous_pid" || return 1
|
|
576
|
+
verify_new_node_storage || {
|
|
577
|
+
echo "The new Leither process has not confirmed the selected storage limit. Rerun fresh setup to finish." >&2
|
|
578
|
+
return 1
|
|
579
|
+
}
|
|
580
|
+
rm -f -- "$pending" || return 1
|
|
581
|
+
echo " Storage ready: ${STORAGE_MAX_GB} GB limit."
|
|
582
|
+
}
|
|
583
|
+
|
|
502
584
|
bootstrap_leither() {
|
|
503
585
|
if (( ! INSTALL_LEITHER || UPGRADE_ONLY || MANAGEMENT_ONLY )); then
|
|
504
586
|
echo "LePan setup requires a running Leither node for this operation." >&2
|
|
@@ -513,7 +595,7 @@ bootstrap_leither() {
|
|
|
513
595
|
LEITHER_WORKDIR=$(node -e 'const fs=require("node:fs"), path=require("node:path"); process.stdout.write(path.dirname(fs.realpathSync(process.argv[1])));' "$(command -v Leither)")
|
|
514
596
|
else
|
|
515
597
|
[[ "${HOME:-}" == /* ]] || { echo "Set --leither-root to an absolute installation directory." >&2; exit 1; }
|
|
516
|
-
LEITHER_WORKDIR="$HOME/.local/share/
|
|
598
|
+
LEITHER_WORKDIR="$HOME/.local/share/leither"
|
|
517
599
|
fi
|
|
518
600
|
fi
|
|
519
601
|
local new_node=0 node_version node_port storage_pending
|
|
@@ -533,7 +615,11 @@ bootstrap_leither() {
|
|
|
533
615
|
# A failed quota choice or configuration write must be safely retryable. The
|
|
534
616
|
# marker identifies only nodes this installer itself began creating; an
|
|
535
617
|
# arbitrary stopped existing node is never treated as fresh.
|
|
536
|
-
if [[ -
|
|
618
|
+
if [[ -L "$storage_pending" || ( -e "$storage_pending" && ! -f "$storage_pending" ) ]]; then
|
|
619
|
+
echo "Invalid Leither storage setup marker at $storage_pending." >&2
|
|
620
|
+
exit 1
|
|
621
|
+
fi
|
|
622
|
+
if [[ -f "$storage_pending" ]]; then new_node=1; fi
|
|
537
623
|
# Validate the selected node's port and check availability before starting it.
|
|
538
624
|
# The process and HTTP version are checked again after startup.
|
|
539
625
|
node_port=$(node -e '
|
|
@@ -556,15 +642,17 @@ try {
|
|
|
556
642
|
echo "Initializing the new private Leither node at $LEITHER_WORKDIR..."
|
|
557
643
|
(umask 077; cd "$LEITHER_WORKDIR" && ./Leither init) || { echo "Leither initialization failed; its files were retained at $LEITHER_WORKDIR." >&2; exit 1; }
|
|
558
644
|
fi
|
|
559
|
-
if [[ ! -s "$LEITHER_WORKDIR/SystemVars.json"
|
|
560
|
-
echo "Leither initialization did not create its
|
|
645
|
+
if [[ ! -s "$LEITHER_WORKDIR/SystemVars.json" && ! -s "$LEITHER_WORKDIR/systemvars.json" ]]; then
|
|
646
|
+
echo "Leither initialization did not create its settings. Inspect $LEITHER_WORKDIR before retrying." >&2
|
|
561
647
|
exit 1
|
|
562
648
|
fi
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
649
|
+
choose_new_node_storage || exit 1
|
|
650
|
+
# init creates only SystemVars. The first run creates the node key and
|
|
651
|
+
# datastore; a stopped retry may already have that datastore available.
|
|
652
|
+
if [[ -e "$LEITHER_WORKDIR/ds/config" ]]; then
|
|
653
|
+
configure_new_node_storage || exit 1
|
|
654
|
+
rm -f -- "$storage_pending"
|
|
655
|
+
fi
|
|
568
656
|
elif [[ -n "$STORAGE_MAX_GB" ]]; then
|
|
569
657
|
echo "--storage-max-gb is only valid when this installer creates a new Leither node." >&2
|
|
570
658
|
echo "Change an existing node's limit from LePan Settings instead." >&2
|
|
@@ -574,20 +662,7 @@ try {
|
|
|
574
662
|
echo "Installing the Leither system service at $LEITHER_WORKDIR..."
|
|
575
663
|
configure_leither_service stopped
|
|
576
664
|
LEITHER_SERVICE_BOOTSTRAPPED=1
|
|
577
|
-
|
|
578
|
-
for attempt in {1..30}; do
|
|
579
|
-
response=$(curl --noproxy '*' -fsS --max-time 2 "http://127.0.0.1:$node_port/getvar?name=ver" 2>/dev/null || true)
|
|
580
|
-
if leither_service_owns_process && node -e 'try { process.exit(JSON.parse(process.argv[1]).replace(/^V/, "") === process.argv[2] ? 0 : 1); } catch { process.exit(1); }' "$response" "$node_version"; then
|
|
581
|
-
ready=1
|
|
582
|
-
break
|
|
583
|
-
fi
|
|
584
|
-
sleep 1
|
|
585
|
-
done
|
|
586
|
-
if (( ! ready )); then
|
|
587
|
-
echo "Leither did not become ready. Its files and service were retained." >&2
|
|
588
|
-
echo "Linux: journalctl -u lifedrive-leither.service. macOS: $LEITHER_WORKDIR/leither-service.log." >&2
|
|
589
|
-
exit 1
|
|
590
|
-
fi
|
|
665
|
+
wait_for_leither "$node_version" "$node_port" || exit 1
|
|
591
666
|
echo "Leither V$node_version is ready on port $node_port. Continuing LePan installation."
|
|
592
667
|
}
|
|
593
668
|
|
|
@@ -596,12 +671,6 @@ if [[ -z "$running_pids" ]]; then
|
|
|
596
671
|
bootstrap_leither
|
|
597
672
|
running_pids=$(find_leither_pids)
|
|
598
673
|
fi
|
|
599
|
-
if [[ -n "$STORAGE_MAX_GB" ]] && (( ! LEITHER_SERVICE_BOOTSTRAPPED )); then
|
|
600
|
-
echo "--storage-max-gb is only valid when this installer creates a new Leither node." >&2
|
|
601
|
-
echo "Change an existing node's limit from LePan Settings instead." >&2
|
|
602
|
-
exit 2
|
|
603
|
-
fi
|
|
604
|
-
|
|
605
674
|
running_roots=()
|
|
606
675
|
while IFS= read -r leither_pid; do
|
|
607
676
|
[[ "$leither_pid" =~ ^[0-9]+$ ]] || continue
|
|
@@ -684,6 +753,19 @@ fi
|
|
|
684
753
|
echo "Found running Leither service at $LEITHER_WORKDIR"
|
|
685
754
|
leither_version "$LEITHER_WORKDIR" >/dev/null || exit 1
|
|
686
755
|
|
|
756
|
+
storage_pending="$LEITHER_WORKDIR/.lifedrive-storage-pending"
|
|
757
|
+
if [[ -L "$storage_pending" ]]; then
|
|
758
|
+
echo "LePan installation stopped: the storage setup marker must not be a symbolic link." >&2
|
|
759
|
+
exit 1
|
|
760
|
+
fi
|
|
761
|
+
if [[ -f "$storage_pending" ]]; then
|
|
762
|
+
finish_new_node_storage || exit 1
|
|
763
|
+
elif [[ -n "$STORAGE_MAX_GB" ]] && (( ! LEITHER_SERVICE_BOOTSTRAPPED )); then
|
|
764
|
+
echo "--storage-max-gb is only valid when this installer creates a new Leither node." >&2
|
|
765
|
+
echo "Change an existing node's limit from LePan Settings instead." >&2
|
|
766
|
+
exit 2
|
|
767
|
+
fi
|
|
768
|
+
|
|
687
769
|
if (( LEITHER_SERVICE_ONLY )); then
|
|
688
770
|
if (( ! LEITHER_SERVICE_BOOTSTRAPPED )); then configure_leither_service running; fi
|
|
689
771
|
configure_macos_leither_firewall
|
|
@@ -719,6 +801,7 @@ cleanup() {
|
|
|
719
801
|
}
|
|
720
802
|
trap cleanup EXIT HUP INT TERM
|
|
721
803
|
|
|
804
|
+
printf '\nInstall LePan\n'
|
|
722
805
|
if [[ "$RELEASE_BASE" == file://* ]]; then
|
|
723
806
|
echo "Loading the LePan release included with the npm package..."
|
|
724
807
|
else
|
|
@@ -759,14 +842,60 @@ if (( MANAGEMENT_ONLY )); then
|
|
|
759
842
|
exit 0
|
|
760
843
|
fi
|
|
761
844
|
|
|
845
|
+
# Existing installations keep their paths (including publisher and service
|
|
846
|
+
# configuration). Fresh installs group application payloads under lepan while
|
|
847
|
+
# retaining the application name used to derive its published identity.
|
|
848
|
+
lepan_dir="$LEITHER_WORKDIR/lepan"
|
|
849
|
+
if [[ ! -d "$LEITHER_WORKDIR/lifeDrive" || -L "$LEITHER_WORKDIR/lifeDrive" ]]; then
|
|
850
|
+
if [[ -L "$lepan_dir" || ( -e "$lepan_dir" && ! -d "$lepan_dir" ) ]]; then
|
|
851
|
+
echo "LePan installation stopped: $lepan_dir must be a regular directory." >&2
|
|
852
|
+
exit 1
|
|
853
|
+
fi
|
|
854
|
+
if [[ -L "$lepan_dir/.installer-layout-v1" || ( -e "$lepan_dir/.installer-layout-v1" && ! -f "$lepan_dir/.installer-layout-v1" ) ]]; then
|
|
855
|
+
echo "LePan installation stopped: invalid layout marker in $lepan_dir." >&2
|
|
856
|
+
exit 1
|
|
857
|
+
fi
|
|
858
|
+
if [[ -d "$lepan_dir" && ! -f "$lepan_dir/.installer-layout-v1" && -n "$(ls -A "$lepan_dir")" ]]; then
|
|
859
|
+
echo "LePan installation stopped: $lepan_dir contains files from an unknown installation." >&2
|
|
860
|
+
exit 1
|
|
861
|
+
fi
|
|
862
|
+
# Stable links keep existing version readers and identity-service paths valid.
|
|
863
|
+
# Refuse conflicting files or links before changing either payload.
|
|
864
|
+
for payload_name in lifeDrive lifedrive-identity; do
|
|
865
|
+
legacy_path="$LEITHER_WORKDIR/$payload_name"
|
|
866
|
+
payload_path="$lepan_dir/$payload_name"
|
|
867
|
+
if [[ -L "$payload_path" || ( -e "$payload_path" && ! -d "$payload_path" ) ]]; then
|
|
868
|
+
echo "LePan installation stopped: unexpected payload at $payload_path." >&2
|
|
869
|
+
exit 1
|
|
870
|
+
fi
|
|
871
|
+
if [[ -L "$legacy_path" ]]; then
|
|
872
|
+
if [[ "$(readlink "$legacy_path")" != "lepan/$payload_name" ]]; then
|
|
873
|
+
echo "LePan installation stopped: $legacy_path points outside the managed LePan layout." >&2
|
|
874
|
+
exit 1
|
|
875
|
+
fi
|
|
876
|
+
elif [[ -e "$legacy_path" ]]; then
|
|
877
|
+
echo "LePan installation stopped: conflicting installation at $legacy_path." >&2
|
|
878
|
+
exit 1
|
|
879
|
+
fi
|
|
880
|
+
done
|
|
881
|
+
(umask 077; mkdir -p "$lepan_dir"; : > "$lepan_dir/.installer-layout-v1")
|
|
882
|
+
for payload_name in lifeDrive lifedrive-identity; do
|
|
883
|
+
(umask 077; mkdir -p "$lepan_dir/$payload_name")
|
|
884
|
+
if [[ ! -L "$LEITHER_WORKDIR/$payload_name" ]]; then
|
|
885
|
+
ln -s "lepan/$payload_name" "$LEITHER_WORKDIR/$payload_name"
|
|
886
|
+
fi
|
|
887
|
+
done
|
|
888
|
+
echo "LePan application files: $lepan_dir"
|
|
889
|
+
fi
|
|
890
|
+
|
|
762
891
|
backup_dir="$LEITHER_WORKDIR/deploy-backups/lifedrive-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
763
|
-
if [[ -d "$LEITHER_WORKDIR/lifeDrive" ]]; then
|
|
764
|
-
mkdir -p "$backup_dir"
|
|
765
|
-
|
|
892
|
+
if [[ -d "$LEITHER_WORKDIR/lifeDrive" && -n "$(ls -A "$LEITHER_WORKDIR/lifeDrive/")" ]]; then
|
|
893
|
+
mkdir -p "$backup_dir/lifeDrive"
|
|
894
|
+
# Copy the contents rather than backing up only the compatibility link.
|
|
895
|
+
cp -R "$LEITHER_WORKDIR/lifeDrive/." "$backup_dir/lifeDrive/"
|
|
766
896
|
echo "Previous LePan files backed up to $backup_dir"
|
|
767
897
|
fi
|
|
768
898
|
|
|
769
|
-
mkdir -p "$LEITHER_WORKDIR/lifeDrive"
|
|
770
899
|
# Remove the previous top-level JavaScript MApp entries before installing the
|
|
771
900
|
# Go dispatcher. Web assets below their hashed asset directory are retained.
|
|
772
901
|
old_entry_files=(
|