@inoku/lepan 0.0.1 → 0.7.59
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 +177 -0
- package/USER_MANUAL.md +399 -0
- package/bin/lifedrive.js +91 -0
- package/dist/lifedrive-bundle.tar.gz +4 -0
- package/dist/lifedrive-bundle.tar.gz.sha256 +1 -0
- package/dist/lifedrive-install.ps1 +478 -0
- package/dist/lifedrive-install.sh +831 -0
- package/package.json +35 -3
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
RELEASE_BASE="${LIFEDRIVE_RELEASE_BASE:-https://github.com/cfa532/lifedrive-installer/releases/latest/download}"
|
|
5
|
+
ARCHIVE_NAME="lifedrive-bundle.tar.gz"
|
|
6
|
+
CHECKSUM_NAME="$ARCHIVE_NAME.sha256"
|
|
7
|
+
LEITHER_WORKDIR="${LIFEDRIVE_WORKDIR:-}"
|
|
8
|
+
UPGRADE_ONLY=0
|
|
9
|
+
MANAGEMENT_ONLY=0
|
|
10
|
+
INSTALL_LEITHER=1
|
|
11
|
+
LEITHER_SERVICE_ONLY=0
|
|
12
|
+
LEITHER_SERVICE_BOOTSTRAPPED=0
|
|
13
|
+
HOUSEHOLD_CONFIG=""
|
|
14
|
+
HOUSEHOLD_SETUP=0
|
|
15
|
+
STORAGE_MAX_GB="${LIFEDRIVE_STORAGE_MAX_GB:-}"
|
|
16
|
+
setup_command_args=()
|
|
17
|
+
setup_arg_count=0
|
|
18
|
+
operation_count=0
|
|
19
|
+
|
|
20
|
+
usage() {
|
|
21
|
+
cat <<'EOF'
|
|
22
|
+
Usage: lifedrive-install.sh [installer options] [setup options]
|
|
23
|
+
|
|
24
|
+
Installer options:
|
|
25
|
+
--leither-root DIR Select an existing node or the directory for a new Leither installation.
|
|
26
|
+
--no-install-leither Require a running Leither node; do not install or start one.
|
|
27
|
+
--leither-service Set up Leither boot startup only; do not install or change LePan.
|
|
28
|
+
--storage-max-gb GB Maximum disk space for a newly installed Leither node (default: 100 GB).
|
|
29
|
+
--release-base URL Alternate GitHub Release asset base URL.
|
|
30
|
+
--upgrade Upgrade an existing LePan without changing device authorization.
|
|
31
|
+
--household Prepare mobile household setup using the existing node address.
|
|
32
|
+
--household-config FILE Install mobile-led household setup using a private service configuration.
|
|
33
|
+
|
|
34
|
+
Setup options are forwarded to lifeDrive-setup.sh. Common examples:
|
|
35
|
+
--registry-url URL
|
|
36
|
+
--publisher-key FILE
|
|
37
|
+
--add-device LABEL
|
|
38
|
+
--identity-out FILE
|
|
39
|
+
--list-devices
|
|
40
|
+
--revoke-device UID
|
|
41
|
+
--skip-domain
|
|
42
|
+
EOF
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
while (( $# )); do
|
|
46
|
+
case "$1" in
|
|
47
|
+
--leither-service)
|
|
48
|
+
LEITHER_SERVICE_ONLY=1
|
|
49
|
+
;;
|
|
50
|
+
--no-install-leither)
|
|
51
|
+
INSTALL_LEITHER=0
|
|
52
|
+
;;
|
|
53
|
+
--household)
|
|
54
|
+
HOUSEHOLD_SETUP=1
|
|
55
|
+
;;
|
|
56
|
+
--household-config)
|
|
57
|
+
shift
|
|
58
|
+
[[ $# -gt 0 && -f "$1" ]] || { echo "--household-config requires a configuration file" >&2; exit 2; }
|
|
59
|
+
HOUSEHOLD_CONFIG="$1"
|
|
60
|
+
;;
|
|
61
|
+
--leither-root)
|
|
62
|
+
shift
|
|
63
|
+
[[ $# -gt 0 && -n "$1" && "$1" != --* ]] || { echo "--leither-root requires a directory" >&2; exit 2; }
|
|
64
|
+
LEITHER_WORKDIR="$1"
|
|
65
|
+
;;
|
|
66
|
+
--storage-max-gb)
|
|
67
|
+
shift
|
|
68
|
+
[[ $# -gt 0 && -n "$1" && "$1" != --* ]] || { echo "--storage-max-gb requires a whole number of gigabytes" >&2; exit 2; }
|
|
69
|
+
STORAGE_MAX_GB="$1"
|
|
70
|
+
;;
|
|
71
|
+
--release-base)
|
|
72
|
+
shift
|
|
73
|
+
[[ $# -gt 0 && -n "$1" && "$1" != --* ]] || { echo "--release-base requires a URL" >&2; exit 2; }
|
|
74
|
+
RELEASE_BASE="${1%/}"
|
|
75
|
+
;;
|
|
76
|
+
--upgrade)
|
|
77
|
+
UPGRADE_ONLY=1
|
|
78
|
+
operation_count=$((operation_count + 1))
|
|
79
|
+
setup_command_args+=("--upgrade")
|
|
80
|
+
setup_arg_count=$((setup_arg_count + 1))
|
|
81
|
+
;;
|
|
82
|
+
--list-devices)
|
|
83
|
+
MANAGEMENT_ONLY=1
|
|
84
|
+
operation_count=$((operation_count + 1))
|
|
85
|
+
setup_command_args+=("$1")
|
|
86
|
+
setup_arg_count=$((setup_arg_count + 1))
|
|
87
|
+
;;
|
|
88
|
+
--add-device|--revoke-device|--registry-url|--publisher-key|--identity-out)
|
|
89
|
+
option="$1"
|
|
90
|
+
shift
|
|
91
|
+
[[ $# -gt 0 && -n "$1" && "$1" != --* ]] || { echo "$option requires a value" >&2; exit 2; }
|
|
92
|
+
if [[ "$option" == --add-device || "$option" == --revoke-device ]]; then
|
|
93
|
+
MANAGEMENT_ONLY=1
|
|
94
|
+
operation_count=$((operation_count + 1))
|
|
95
|
+
fi
|
|
96
|
+
setup_command_args+=("$option" "$1")
|
|
97
|
+
setup_arg_count=$((setup_arg_count + 2))
|
|
98
|
+
;;
|
|
99
|
+
--skip-domain) setup_command_args+=("$1"); setup_arg_count=$((setup_arg_count + 1)) ;;
|
|
100
|
+
-h|--help) usage; exit 0 ;;
|
|
101
|
+
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
|
|
102
|
+
esac
|
|
103
|
+
shift
|
|
104
|
+
done
|
|
105
|
+
|
|
106
|
+
if (( operation_count > 1 )); then
|
|
107
|
+
echo "Choose only one of --upgrade, --add-device, --list-devices, or --revoke-device." >&2
|
|
108
|
+
exit 2
|
|
109
|
+
fi
|
|
110
|
+
if (( LEITHER_SERVICE_ONLY )) && { (( ! INSTALL_LEITHER || operation_count || HOUSEHOLD_SETUP || setup_arg_count )) || [[ -n "$HOUSEHOLD_CONFIG" || -n "$STORAGE_MAX_GB" ]]; }; then
|
|
111
|
+
echo "Use --leither-service with only --leither-root and installer source options." >&2
|
|
112
|
+
exit 2
|
|
113
|
+
fi
|
|
114
|
+
if (( HOUSEHOLD_SETUP )) && { [[ -n "$HOUSEHOLD_CONFIG" ]] || (( MANAGEMENT_ONLY )); }; then
|
|
115
|
+
echo "Use --household separately from --household-config and device-management options." >&2
|
|
116
|
+
exit 2
|
|
117
|
+
fi
|
|
118
|
+
valid_storage_gb() {
|
|
119
|
+
local value="$1"
|
|
120
|
+
[[ "$value" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
121
|
+
(( ${#value} < 11 )) && return 0
|
|
122
|
+
(( ${#value} == 11 )) && (( 10#$value <= 18446744073 ))
|
|
123
|
+
}
|
|
124
|
+
if [[ -n "$STORAGE_MAX_GB" ]] && ! valid_storage_gb "$STORAGE_MAX_GB"; then
|
|
125
|
+
echo "Leither storage must be a positive whole number of gigabytes." >&2
|
|
126
|
+
exit 2
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
for required in node curl tar; do
|
|
130
|
+
command -v "$required" >/dev/null 2>&1 || { echo "LePan installation requires $required." >&2; exit 1; }
|
|
131
|
+
done
|
|
132
|
+
|
|
133
|
+
find_leither_pids() {
|
|
134
|
+
ps -axww -o pid=,comm= 2>/dev/null | awk '
|
|
135
|
+
{ pid = $1; sub(/^[[:space:]]*[0-9]+[[:space:]]+/, "") }
|
|
136
|
+
$0 == "Leither" || $0 ~ /\/Leither$/ { print pid }
|
|
137
|
+
'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
leither_version() {
|
|
141
|
+
local version_json
|
|
142
|
+
version_json=$(cd "$1" && ./Leither version --json) || return 1
|
|
143
|
+
node -e '
|
|
144
|
+
try {
|
|
145
|
+
const reply = JSON.parse(process.argv[1]);
|
|
146
|
+
const reported = reply?.data?.version ?? reply?.version;
|
|
147
|
+
if (typeof reported !== "string") throw new Error("missing version");
|
|
148
|
+
const version = reported.replace(/^V/, "");
|
|
149
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error("unknown version");
|
|
150
|
+
const [major, minor, patch] = version.split(".").map(Number);
|
|
151
|
+
if (major === 0 && (minor < 24 || (minor === 24 && patch < 11))) throw new Error("too old");
|
|
152
|
+
process.stdout.write(version);
|
|
153
|
+
} catch {
|
|
154
|
+
console.error("LePan requires Leither V0.24.11 or newer. Upgrade Leither separately; existing nodes are never overwritten.");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
' "$version_json"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
available_storage_gb() {
|
|
161
|
+
# POSIX df reports 1024-byte blocks with -k. The result is informational and
|
|
162
|
+
# also prevents a fresh node from receiving a quota the selected disk cannot
|
|
163
|
+
# currently hold. Failure to inspect the filesystem does not invent a value.
|
|
164
|
+
LC_ALL=C df -Pk "$LEITHER_WORKDIR" 2>/dev/null | awk 'NR == 2 && $4 ~ /^[0-9]+$/ { print int(($4 * 1024) / 1000000000) }'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
configure_new_node_storage() {
|
|
168
|
+
local available_gb selected_gb default_gb=100 config
|
|
169
|
+
config="$LEITHER_WORKDIR/ds/config"
|
|
170
|
+
[[ -f "$config" && ! -L "$config" ]] || {
|
|
171
|
+
echo "Leither initialization did not create a regular datastore configuration at $config." >&2
|
|
172
|
+
return 1
|
|
173
|
+
}
|
|
174
|
+
available_gb=$(available_storage_gb)
|
|
175
|
+
echo
|
|
176
|
+
echo "Choose the maximum hard-drive space Leither may use for LePan and any other data on this node."
|
|
177
|
+
if [[ "$available_gb" =~ ^[0-9]+$ ]]; then
|
|
178
|
+
echo "Available now on the selected drive: ${available_gb} GB."
|
|
179
|
+
fi
|
|
180
|
+
if [[ -n "$STORAGE_MAX_GB" ]]; then
|
|
181
|
+
selected_gb="$STORAGE_MAX_GB"
|
|
182
|
+
echo "Leither storage limit: ${selected_gb} GB."
|
|
183
|
+
elif [[ -t 0 ]]; then
|
|
184
|
+
while true; do
|
|
185
|
+
read -r -p "Maximum Leither storage in GB [$default_gb]: " selected_gb
|
|
186
|
+
selected_gb="${selected_gb:-$default_gb}"
|
|
187
|
+
if ! valid_storage_gb "$selected_gb"; then
|
|
188
|
+
echo "Enter a positive whole number of gigabytes." >&2
|
|
189
|
+
continue
|
|
190
|
+
fi
|
|
191
|
+
if [[ "$available_gb" =~ ^[0-9]+$ ]] && (( selected_gb > available_gb )); then
|
|
192
|
+
echo "That exceeds the ${available_gb} GB currently available on this drive. Choose a smaller limit." >&2
|
|
193
|
+
continue
|
|
194
|
+
fi
|
|
195
|
+
break
|
|
196
|
+
done
|
|
197
|
+
else
|
|
198
|
+
selected_gb="$default_gb"
|
|
199
|
+
echo "No interactive terminal was detected; using the ${default_gb} GB default."
|
|
200
|
+
fi
|
|
201
|
+
if [[ "$available_gb" =~ ^[0-9]+$ ]] && (( selected_gb > available_gb )); then
|
|
202
|
+
echo "The selected limit (${selected_gb} GB) exceeds the ${available_gb} GB currently available on this drive." >&2
|
|
203
|
+
echo "Free space or rerun setup with --storage-max-gb set to a smaller value." >&2
|
|
204
|
+
return 1
|
|
205
|
+
fi
|
|
206
|
+
node - "$config" "${selected_gb}GB" <<'NODE'
|
|
207
|
+
const fs = require("node:fs"), path = require("node:path");
|
|
208
|
+
const file = process.argv[2], desired = process.argv[3];
|
|
209
|
+
const info = fs.lstatSync(file);
|
|
210
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error("Leither datastore configuration is not a regular file");
|
|
211
|
+
const source = fs.readFileSync(file, "utf8");
|
|
212
|
+
const parsed = JSON.parse(source);
|
|
213
|
+
const current = parsed?.Datastore?.StorageMax;
|
|
214
|
+
if (typeof current !== "string") throw new Error("Leither datastore StorageMax is not a string");
|
|
215
|
+
const key = '"StorageMax"';
|
|
216
|
+
const keyIndex = source.indexOf(key);
|
|
217
|
+
if (keyIndex < 0 || source.indexOf(key, keyIndex + key.length) >= 0) throw new Error("Leither datastore StorageMax must occur exactly once");
|
|
218
|
+
const currentJSON = JSON.stringify(current), nextJSON = JSON.stringify(desired);
|
|
219
|
+
const valueIndex = source.indexOf(currentJSON, keyIndex + key.length);
|
|
220
|
+
if (valueIndex < 0) throw new Error("Leither datastore StorageMax value could not be located");
|
|
221
|
+
const next = source.slice(0, valueIndex) + nextJSON + source.slice(valueIndex + currentJSON.length);
|
|
222
|
+
const verified = JSON.parse(next);
|
|
223
|
+
if (verified?.Datastore?.StorageMax !== desired) throw new Error("updated Leither datastore configuration did not validate");
|
|
224
|
+
const temporary = `${file}.lifedrive-install-${process.pid}`;
|
|
225
|
+
let descriptor;
|
|
226
|
+
try {
|
|
227
|
+
descriptor = fs.openSync(temporary, "wx", info.mode & 0o777);
|
|
228
|
+
fs.writeFileSync(descriptor, next, "utf8");
|
|
229
|
+
fs.fsyncSync(descriptor);
|
|
230
|
+
fs.closeSync(descriptor);
|
|
231
|
+
descriptor = undefined;
|
|
232
|
+
fs.renameSync(temporary, file);
|
|
233
|
+
try {
|
|
234
|
+
const directory = fs.openSync(path.dirname(file), "r");
|
|
235
|
+
fs.fsyncSync(directory);
|
|
236
|
+
fs.closeSync(directory);
|
|
237
|
+
} catch {}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
240
|
+
try { fs.unlinkSync(temporary); } catch {}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
NODE
|
|
244
|
+
echo "Leither storage limit saved as ${selected_gb} GB."
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
install_leither_binary() (
|
|
248
|
+
# Follow http://vzhan.cn/#start.html and its install.sh download layout.
|
|
249
|
+
# Download only the binary and checksum; initialization stays in the final
|
|
250
|
+
# private node directory, never in a temporary directory or release bundle.
|
|
251
|
+
set -euo pipefail
|
|
252
|
+
umask 077
|
|
253
|
+
case "$(uname -s)" in Linux) leither_os=linux ;; Darwin) leither_os=darwin ;; *) echo "Unsupported Leither platform." >&2; exit 1 ;; esac
|
|
254
|
+
case "$(uname -m)" in x86_64|amd64) leither_arch=amd64 ;; arm64|aarch64) leither_arch=arm64 ;; *) echo "Unsupported Leither architecture." >&2; exit 1 ;; esac
|
|
255
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
256
|
+
checksum_command=(sha256sum)
|
|
257
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
258
|
+
checksum_command=(shasum -a 256)
|
|
259
|
+
else
|
|
260
|
+
echo "Leither installation requires sha256sum or shasum." >&2
|
|
261
|
+
exit 1
|
|
262
|
+
fi
|
|
263
|
+
download_dir=$(mktemp -d "${TMPDIR:-/tmp}/lifedrive-leither.XXXXXX")
|
|
264
|
+
staged_binary=""
|
|
265
|
+
trap 'if [[ -n "$staged_binary" ]]; then rm -f -- "$staged_binary"; fi; rm -f -- "$download_dir/versions.html" "$download_dir/Leither" "$download_dir/Leither.sha256"; rmdir -- "$download_dir"' EXIT
|
|
266
|
+
base_url="http://vzhan.cn/mm/Fc1BRTFafOGzq5P8KmkVJqwS2v2"
|
|
267
|
+
echo "Finding the latest Leither release from vzhan.cn..."
|
|
268
|
+
curl -fsSL --retry 3 --connect-timeout 20 --max-time 60 "$base_url/" -o "$download_dir/versions.html"
|
|
269
|
+
latest_version=$(node -e '
|
|
270
|
+
const fs = require("node:fs");
|
|
271
|
+
const html = fs.readFileSync(process.argv[1], "utf8");
|
|
272
|
+
const versions = [...html.matchAll(/href=["\x27]V(\d+\.\d+\.\d+)(?:\/|["\x27])/g)].map(match => match[1]);
|
|
273
|
+
versions.sort((a, b) => {
|
|
274
|
+
const left = a.split(".").map(Number), right = b.split(".").map(Number);
|
|
275
|
+
return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
|
|
276
|
+
});
|
|
277
|
+
if (!versions.length) { console.error("No Leither release was found in the official index."); process.exit(1); }
|
|
278
|
+
process.stdout.write(`V${versions[versions.length - 1]}`);
|
|
279
|
+
' "$download_dir/versions.html")
|
|
280
|
+
download_url="$base_url/$latest_version/Leither.$leither_os.$leither_arch"
|
|
281
|
+
echo "Downloading Leither $latest_version for $leither_os/$leither_arch..."
|
|
282
|
+
curl -fsSL --retry 3 --connect-timeout 20 --max-time 600 "$download_url" -o "$download_dir/Leither"
|
|
283
|
+
curl -fsSL --retry 3 --connect-timeout 20 --max-time 60 "$download_url.sha256" -o "$download_dir/Leither.sha256"
|
|
284
|
+
expected=$(awk 'NR==1 {print $1}' "$download_dir/Leither.sha256")
|
|
285
|
+
actual=$("${checksum_command[@]}" "$download_dir/Leither" | awk '{print $1}')
|
|
286
|
+
if [[ ! "$expected" =~ ^[a-fA-F0-9]{64}$ || "$actual" != "$expected" ]]; then
|
|
287
|
+
echo "Leither SHA-256 verification failed; the binary was not installed." >&2
|
|
288
|
+
exit 1
|
|
289
|
+
fi
|
|
290
|
+
# Install a complete binary atomically on the destination filesystem. The
|
|
291
|
+
# hard link fails if another installer has already created Leither.
|
|
292
|
+
staged_binary=$(mktemp "$LEITHER_WORKDIR/.Leither.XXXXXX")
|
|
293
|
+
cat "$download_dir/Leither" > "$staged_binary"
|
|
294
|
+
chmod 700 "$staged_binary"
|
|
295
|
+
node -e 'require("node:fs").linkSync(process.argv[1], process.argv[2]);' "$staged_binary" "$LEITHER_WORKDIR/Leither"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
leither_service_prerequisites() {
|
|
299
|
+
if [[ $(id -u) == 0 ]]; then
|
|
300
|
+
echo "Run setup as the account that owns the Leither node; sudo is used only to install its system service." >&2
|
|
301
|
+
exit 1
|
|
302
|
+
fi
|
|
303
|
+
command -v sudo >/dev/null || { echo "System service setup requires sudo." >&2; exit 1; }
|
|
304
|
+
case "$(uname -s)" in
|
|
305
|
+
Linux)
|
|
306
|
+
if ! command -v systemctl >/dev/null || [[ ! -d /run/systemd/system ]]; then
|
|
307
|
+
echo "Automatic Leither startup requires Linux with systemd. On other systems, start Leither with your service manager and use --no-install-leither." >&2
|
|
308
|
+
exit 1
|
|
309
|
+
fi
|
|
310
|
+
systemctl show --property=Version --value >/dev/null || { echo "The systemd service manager is not reachable." >&2; exit 1; }
|
|
311
|
+
;;
|
|
312
|
+
Darwin)
|
|
313
|
+
command -v launchctl >/dev/null && command -v plutil >/dev/null && command -v lsof >/dev/null || { echo "macOS service setup requires launchctl, plutil, and lsof." >&2; exit 1; }
|
|
314
|
+
;;
|
|
315
|
+
*) echo "Unsupported service platform." >&2; exit 1 ;;
|
|
316
|
+
esac
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
leither_service_pid() {
|
|
320
|
+
case "$(uname -s)" in
|
|
321
|
+
Linux) systemctl show lifedrive-leither.service --property=MainPID --value 2>/dev/null ;;
|
|
322
|
+
Darwin) launchctl print system/uk.inoku.leither 2>/dev/null | awk '/^[[:space:]]*pid = / {print $3}' ;;
|
|
323
|
+
esac
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
leither_service_owns_process() {
|
|
327
|
+
local service_pid executable process_root
|
|
328
|
+
service_pid=$(leither_service_pid) || return 1
|
|
329
|
+
[[ "$service_pid" =~ ^[0-9]+$ && "$service_pid" != 0 ]] || return 1
|
|
330
|
+
if [[ "$(uname -s)" == Linux ]]; then
|
|
331
|
+
executable=$(readlink "/proc/$service_pid/exe") || return 1
|
|
332
|
+
executable="${executable% (deleted)}"
|
|
333
|
+
process_root=$(readlink "/proc/$service_pid/cwd") || return 1
|
|
334
|
+
else
|
|
335
|
+
executable=$(ps -ww -p "$service_pid" -o comm= | sed 's/^[[:space:]]*//') || return 1
|
|
336
|
+
process_root=$(lsof -a -p "$service_pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p') || return 1
|
|
337
|
+
fi
|
|
338
|
+
[[ "$executable" == "$LEITHER_WORKDIR/Leither" && "$process_root" == "$LEITHER_WORKDIR" ]]
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
configure_leither_service() (
|
|
342
|
+
set -euo pipefail
|
|
343
|
+
umask 077
|
|
344
|
+
mode="$1"
|
|
345
|
+
leither_service_prerequisites
|
|
346
|
+
service_os=$(uname -s)
|
|
347
|
+
account=$(id -un)
|
|
348
|
+
service_home=$(node -e 'process.stdout.write(require("node:os").userInfo().homedir);')
|
|
349
|
+
service_work=$(mktemp -d "${TMPDIR:-/tmp}/lifedrive-leither-service.XXXXXX")
|
|
350
|
+
trap 'rm -f -- "$service_work/lifedrive-leither.service" "$service_work/leither.plist"; rmdir -- "$service_work"' EXIT
|
|
351
|
+
|
|
352
|
+
if [[ "$service_os" == Linux ]]; then
|
|
353
|
+
unit="$service_work/lifedrive-leither.service"
|
|
354
|
+
installed_unit=/etc/systemd/system/lifedrive-leither.service
|
|
355
|
+
# Reject systemd specifiers, variable expansion, escapes and line breaks.
|
|
356
|
+
# Ordinary spaces in directory names remain supported through quoting.
|
|
357
|
+
case "$LEITHER_WORKDIR$service_home$account" in *$'\n'*|*$'\r'*|*'"'*|*'%'*|*'$'*|*'\'*) echo "Unsupported character in the Leither systemd service path or account." >&2; exit 1 ;; esac
|
|
358
|
+
cat > "$unit" <<UNIT
|
|
359
|
+
[Unit]
|
|
360
|
+
Description=Leither node installed by LifeDrive
|
|
361
|
+
Wants=network-online.target
|
|
362
|
+
After=network-online.target
|
|
363
|
+
[Service]
|
|
364
|
+
Type=simple
|
|
365
|
+
User=$account
|
|
366
|
+
WorkingDirectory="$LEITHER_WORKDIR"
|
|
367
|
+
Environment="HOME=$service_home"
|
|
368
|
+
ExecStart="$LEITHER_WORKDIR/Leither" run
|
|
369
|
+
UMask=0077
|
|
370
|
+
Restart=on-failure
|
|
371
|
+
RestartSec=5
|
|
372
|
+
TimeoutStopSec=60
|
|
373
|
+
[Install]
|
|
374
|
+
WantedBy=multi-user.target
|
|
375
|
+
UNIT
|
|
376
|
+
existing_unit=$(systemctl show lifedrive-leither.service --property=FragmentPath --value 2>/dev/null || true)
|
|
377
|
+
dropins=$(systemctl show lifedrive-leither.service --property=DropInPaths --value 2>/dev/null || true)
|
|
378
|
+
if [[ -n "$dropins" ]] ||
|
|
379
|
+
{ [[ -n "$existing_unit" ]] && ! cmp -s "$unit" "$existing_unit"; } ||
|
|
380
|
+
{ [[ -e "$installed_unit" || -L "$installed_unit" ]] && ! cmp -s "$unit" "$installed_unit"; }; then
|
|
381
|
+
echo "An existing lifedrive-leither.service has a different configuration. It was not replaced." >&2
|
|
382
|
+
exit 1
|
|
383
|
+
fi
|
|
384
|
+
if [[ "$mode" == running ]] && ! leither_service_owns_process; then
|
|
385
|
+
echo "Leither is already running outside lifedrive-leither.service. Its process was left unchanged." >&2
|
|
386
|
+
echo "Keep its existing service manager, or stop it during a maintenance window and rerun --leither-service --leither-root for this node." >&2
|
|
387
|
+
exit 1
|
|
388
|
+
fi
|
|
389
|
+
if command -v systemd-analyze >/dev/null; then systemd-analyze verify "$unit"; fi
|
|
390
|
+
if [[ -z "$existing_unit" ]]; then
|
|
391
|
+
sudo install -o root -g root -m 644 "$unit" "$installed_unit"
|
|
392
|
+
fi
|
|
393
|
+
sudo systemctl daemon-reload
|
|
394
|
+
sudo systemctl enable lifedrive-leither.service
|
|
395
|
+
if [[ "$mode" == stopped ]]; then sudo systemctl start lifedrive-leither.service; fi
|
|
396
|
+
echo "Leither boot startup enabled: lifedrive-leither.service (user $account)."
|
|
397
|
+
else
|
|
398
|
+
plist="$service_work/leither.plist"
|
|
399
|
+
installed_plist=/Library/LaunchDaemons/uk.inoku.leither.plist
|
|
400
|
+
node - "$account" "$service_home" "$LEITHER_WORKDIR" <<'NODE' > "$plist"
|
|
401
|
+
const [account, home, root] = process.argv.slice(2);
|
|
402
|
+
process.stdout.write(JSON.stringify({
|
|
403
|
+
Label: "uk.inoku.leither",
|
|
404
|
+
UserName: account,
|
|
405
|
+
ProgramArguments: [`${root}/Leither`, "run"],
|
|
406
|
+
WorkingDirectory: root,
|
|
407
|
+
EnvironmentVariables: { HOME: home, PATH: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" },
|
|
408
|
+
RunAtLoad: true,
|
|
409
|
+
KeepAlive: true,
|
|
410
|
+
ThrottleInterval: 5,
|
|
411
|
+
ExitTimeOut: 60,
|
|
412
|
+
Umask: 63,
|
|
413
|
+
StandardOutPath: `${root}/leither-service.log`,
|
|
414
|
+
StandardErrorPath: `${root}/leither-service.log`
|
|
415
|
+
}));
|
|
416
|
+
NODE
|
|
417
|
+
plutil -convert xml1 "$plist"
|
|
418
|
+
plutil -lint "$plist" >/dev/null
|
|
419
|
+
if [[ -e "$installed_plist" || -L "$installed_plist" ]]; then
|
|
420
|
+
if ! cmp -s "$plist" "$installed_plist"; then
|
|
421
|
+
echo "An existing uk.inoku.leither daemon has a different configuration. It was not replaced." >&2
|
|
422
|
+
exit 1
|
|
423
|
+
fi
|
|
424
|
+
elif launchctl print system/uk.inoku.leither >/dev/null 2>&1; then
|
|
425
|
+
echo "An unmanaged uk.inoku.leither daemon is already loaded. It was left unchanged." >&2
|
|
426
|
+
exit 1
|
|
427
|
+
fi
|
|
428
|
+
if [[ "$mode" == running ]] && ! leither_service_owns_process; then
|
|
429
|
+
echo "Leither is already running outside uk.inoku.leither. Its process was left unchanged." >&2
|
|
430
|
+
echo "Keep its existing service manager, or stop it during a maintenance window and rerun --leither-service --leither-root for this node." >&2
|
|
431
|
+
exit 1
|
|
432
|
+
fi
|
|
433
|
+
if [[ ! -e "$installed_plist" ]]; then sudo install -o root -g wheel -m 644 "$plist" "$installed_plist"; fi
|
|
434
|
+
sudo launchctl enable system/uk.inoku.leither
|
|
435
|
+
if [[ "$mode" == stopped ]]; then
|
|
436
|
+
if launchctl print system/uk.inoku.leither >/dev/null 2>&1; then
|
|
437
|
+
sudo launchctl kickstart system/uk.inoku.leither
|
|
438
|
+
else
|
|
439
|
+
sudo launchctl bootstrap system "$installed_plist"
|
|
440
|
+
fi
|
|
441
|
+
fi
|
|
442
|
+
echo "Leither boot startup enabled: uk.inoku.leither (user $account)."
|
|
443
|
+
fi
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
configure_macos_leither_firewall() {
|
|
447
|
+
[[ "$(uname -s)" == Darwin ]] || return 0
|
|
448
|
+
local firewall=/usr/libexec/ApplicationFirewall/socketfilterfw
|
|
449
|
+
local leither_binary firewall_apps firewall_state block_all_state
|
|
450
|
+
[[ -x "$firewall" ]] || {
|
|
451
|
+
echo "LePan setup could not find the macOS Application Firewall tool at $firewall." >&2
|
|
452
|
+
return 1
|
|
453
|
+
}
|
|
454
|
+
command -v sudo >/dev/null 2>&1 || {
|
|
455
|
+
echo "LePan setup requires sudo to allow Leither through the macOS Application Firewall." >&2
|
|
456
|
+
return 1
|
|
457
|
+
}
|
|
458
|
+
leither_binary=$(node -e 'process.stdout.write(require("node:fs").realpathSync(process.argv[1]));' "$LEITHER_WORKDIR/Leither") || {
|
|
459
|
+
echo "LePan setup could not resolve the installed Leither executable." >&2
|
|
460
|
+
return 1
|
|
461
|
+
}
|
|
462
|
+
[[ "$leither_binary" == /* && -x "$leither_binary" ]] || {
|
|
463
|
+
echo "LePan setup could not access the installed Leither executable at $leither_binary." >&2
|
|
464
|
+
return 1
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
echo "Allowing Leither through the macOS Application Firewall..."
|
|
468
|
+
firewall_apps=$("$firewall" --listapps) || {
|
|
469
|
+
echo "LePan setup could not read the macOS Application Firewall rules." >&2
|
|
470
|
+
return 1
|
|
471
|
+
}
|
|
472
|
+
if [[ "$firewall_apps" != *"$leither_binary"* ]]; then
|
|
473
|
+
sudo "$firewall" --add "$leither_binary" || {
|
|
474
|
+
echo "LePan setup could not add Leither to the macOS Application Firewall." >&2
|
|
475
|
+
return 1
|
|
476
|
+
}
|
|
477
|
+
fi
|
|
478
|
+
sudo "$firewall" --unblockapp "$leither_binary" || {
|
|
479
|
+
echo "LePan setup could not allow incoming connections to Leither in the macOS Application Firewall." >&2
|
|
480
|
+
return 1
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
firewall_apps=$("$firewall" --listapps) || return 1
|
|
484
|
+
[[ "$firewall_apps" == *"$leither_binary"* ]] || {
|
|
485
|
+
echo "LePan setup could not verify Leither in the macOS Application Firewall rules." >&2
|
|
486
|
+
return 1
|
|
487
|
+
}
|
|
488
|
+
firewall_state=$("$firewall" --getappblocked "$leither_binary") || return 1
|
|
489
|
+
[[ "$firewall_state" == *"is permitted"* ]] || {
|
|
490
|
+
echo "The macOS Application Firewall still reports Leither as blocked." >&2
|
|
491
|
+
return 1
|
|
492
|
+
}
|
|
493
|
+
echo "macOS Application Firewall allows incoming connections to Leither."
|
|
494
|
+
|
|
495
|
+
block_all_state=$("$firewall" --getblockall 2>/dev/null || true)
|
|
496
|
+
if [[ "$block_all_state" == *"enabled"* ]]; then
|
|
497
|
+
echo "Warning: macOS is configured to block all incoming connections." >&2
|
|
498
|
+
echo "Disable that setting before using LePan from another device." >&2
|
|
499
|
+
fi
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
bootstrap_leither() {
|
|
503
|
+
if (( ! INSTALL_LEITHER || UPGRADE_ONLY || MANAGEMENT_ONLY )); then
|
|
504
|
+
echo "LePan setup requires a running Leither node for this operation." >&2
|
|
505
|
+
echo "Start the existing node, or run a fresh installation without --upgrade or device-management options." >&2
|
|
506
|
+
exit 1
|
|
507
|
+
fi
|
|
508
|
+
leither_service_prerequisites
|
|
509
|
+
if [[ -z "$LEITHER_WORKDIR" ]]; then
|
|
510
|
+
if [[ -e ./Leither || -e ./SystemVars.json || -e ./systemvars.json || -e ./hostkey.cfg ]]; then
|
|
511
|
+
LEITHER_WORKDIR="$PWD"
|
|
512
|
+
elif command -v Leither >/dev/null 2>&1; then
|
|
513
|
+
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
|
+
else
|
|
515
|
+
[[ "${HOME:-}" == /* ]] || { echo "Set --leither-root to an absolute installation directory." >&2; exit 1; }
|
|
516
|
+
LEITHER_WORKDIR="$HOME/.local/share/lifedrive/leither"
|
|
517
|
+
fi
|
|
518
|
+
fi
|
|
519
|
+
local new_node=0 node_version node_port storage_pending
|
|
520
|
+
if [[ ! -e "$LEITHER_WORKDIR/Leither" && ! -L "$LEITHER_WORKDIR/Leither" ]]; then
|
|
521
|
+
if [[ -d "$LEITHER_WORKDIR" && -n "$(ls -A "$LEITHER_WORKDIR")" ]]; then
|
|
522
|
+
echo "Leither is missing, but $LEITHER_WORKDIR contains existing files. Choose an empty --leither-root; nothing was replaced." >&2
|
|
523
|
+
exit 1
|
|
524
|
+
fi
|
|
525
|
+
(umask 077; mkdir -p "$LEITHER_WORKDIR")
|
|
526
|
+
new_node=1
|
|
527
|
+
elif [[ ! -x "$LEITHER_WORKDIR/Leither" ]]; then
|
|
528
|
+
echo "Leither exists at $LEITHER_WORKDIR but is not executable. Correct its permissions and rerun setup." >&2
|
|
529
|
+
exit 1
|
|
530
|
+
fi
|
|
531
|
+
LEITHER_WORKDIR=$(cd "$LEITHER_WORKDIR" && pwd -P)
|
|
532
|
+
storage_pending="$LEITHER_WORKDIR/.lifedrive-storage-pending"
|
|
533
|
+
# A failed quota choice or configuration write must be safely retryable. The
|
|
534
|
+
# marker identifies only nodes this installer itself began creating; an
|
|
535
|
+
# arbitrary stopped existing node is never treated as fresh.
|
|
536
|
+
if [[ -f "$storage_pending" && ! -L "$storage_pending" ]]; then new_node=1; fi
|
|
537
|
+
# Validate the selected node's port and check availability before starting it.
|
|
538
|
+
# The process and HTTP version are checked again after startup.
|
|
539
|
+
node_port=$(node -e '
|
|
540
|
+
const fs = require("node:fs"), path = require("node:path"), net = require("node:net");
|
|
541
|
+
try {
|
|
542
|
+
const root = process.argv[1];
|
|
543
|
+
let port = 4800;
|
|
544
|
+
const vars = ["SystemVars.json", "systemvars.json"].map(name => path.join(root, name)).find(file => fs.existsSync(file));
|
|
545
|
+
if (vars) port = JSON.parse(fs.readFileSync(vars, "utf8")).ServicePort ?? port;
|
|
546
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid Leither ServicePort");
|
|
547
|
+
const server = net.createServer();
|
|
548
|
+
server.on("error", () => { console.error(`Leither port ${port} is unavailable. Free it or configure a different ServicePort in the selected node’s SystemVars.json.`); process.exitCode = 1; });
|
|
549
|
+
server.listen(port, "127.0.0.1", () => server.close(() => process.stdout.write(String(port))));
|
|
550
|
+
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
551
|
+
' "$LEITHER_WORKDIR") || exit 1
|
|
552
|
+
if (( new_node )); then
|
|
553
|
+
if [[ ! -x "$LEITHER_WORKDIR/Leither" ]]; then install_leither_binary; fi
|
|
554
|
+
if [[ ! -e "$storage_pending" ]]; then (umask 077; : > "$storage_pending"); fi
|
|
555
|
+
if [[ ! -s "$LEITHER_WORKDIR/SystemVars.json" && ! -s "$LEITHER_WORKDIR/hostkey.cfg" && ! -e "$LEITHER_WORKDIR/ds/config" ]]; then
|
|
556
|
+
echo "Initializing the new private Leither node at $LEITHER_WORKDIR..."
|
|
557
|
+
(umask 077; cd "$LEITHER_WORKDIR" && ./Leither init) || { echo "Leither initialization failed; its files were retained at $LEITHER_WORKDIR." >&2; exit 1; }
|
|
558
|
+
fi
|
|
559
|
+
if [[ ! -s "$LEITHER_WORKDIR/SystemVars.json" || ! -s "$LEITHER_WORKDIR/hostkey.cfg" ]]; then
|
|
560
|
+
echo "Leither initialization did not create its configuration and node key. Inspect $LEITHER_WORKDIR before retrying." >&2
|
|
561
|
+
exit 1
|
|
562
|
+
fi
|
|
563
|
+
configure_new_node_storage || {
|
|
564
|
+
echo "Leither storage setup failed; the initialized node was retained but not started." >&2
|
|
565
|
+
exit 1
|
|
566
|
+
}
|
|
567
|
+
rm -f -- "$storage_pending" || { echo "Could not finish the new-node storage configuration." >&2; exit 1; }
|
|
568
|
+
elif [[ -n "$STORAGE_MAX_GB" ]]; then
|
|
569
|
+
echo "--storage-max-gb is only valid when this installer creates a new Leither node." >&2
|
|
570
|
+
echo "Change an existing node's limit from LePan Settings instead." >&2
|
|
571
|
+
exit 2
|
|
572
|
+
fi
|
|
573
|
+
node_version=$(leither_version "$LEITHER_WORKDIR") || exit 1
|
|
574
|
+
echo "Installing the Leither system service at $LEITHER_WORKDIR..."
|
|
575
|
+
configure_leither_service stopped
|
|
576
|
+
LEITHER_SERVICE_BOOTSTRAPPED=1
|
|
577
|
+
local ready=0 attempt response
|
|
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
|
|
591
|
+
echo "Leither V$node_version is ready on port $node_port. Continuing LePan installation."
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
running_pids=$(find_leither_pids)
|
|
595
|
+
if [[ -z "$running_pids" ]]; then
|
|
596
|
+
bootstrap_leither
|
|
597
|
+
running_pids=$(find_leither_pids)
|
|
598
|
+
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
|
+
running_roots=()
|
|
606
|
+
while IFS= read -r leither_pid; do
|
|
607
|
+
[[ "$leither_pid" =~ ^[0-9]+$ ]] || continue
|
|
608
|
+
leither_executable=""
|
|
609
|
+
leither_cwd=""
|
|
610
|
+
if [[ -e "/proc/$leither_pid/exe" ]]; then
|
|
611
|
+
leither_executable=$(readlink "/proc/$leither_pid/exe" 2>/dev/null || true)
|
|
612
|
+
leither_executable="${leither_executable% (deleted)}"
|
|
613
|
+
fi
|
|
614
|
+
if [[ -z "$leither_executable" ]]; then
|
|
615
|
+
leither_executable=$(ps -ww -p "$leither_pid" -o comm= 2>/dev/null | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' || true)
|
|
616
|
+
fi
|
|
617
|
+
if [[ -z "$leither_executable" || "$leither_executable" != /* ]]; then
|
|
618
|
+
leither_command=$(ps -p "$leither_pid" -o args= 2>/dev/null | sed -e 's/^[[:space:]]*//' || true)
|
|
619
|
+
command_executable="${leither_command%% *}"
|
|
620
|
+
if [[ "$command_executable" == /* ]]; then
|
|
621
|
+
leither_executable="$command_executable"
|
|
622
|
+
fi
|
|
623
|
+
fi
|
|
624
|
+
if [[ -L "/proc/$leither_pid/cwd" ]]; then
|
|
625
|
+
leither_cwd=$(readlink "/proc/$leither_pid/cwd" 2>/dev/null || true)
|
|
626
|
+
fi
|
|
627
|
+
if [[ -z "$leither_cwd" ]] && command -v lsof >/dev/null 2>&1; then
|
|
628
|
+
leither_cwd=$(lsof -a -p "$leither_pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1 || true)
|
|
629
|
+
fi
|
|
630
|
+
|
|
631
|
+
discovered_root=""
|
|
632
|
+
if [[ "$leither_executable" == /* && "${leither_executable##*/}" == "Leither" ]]; then
|
|
633
|
+
executable_dir="${leither_executable%/*}"
|
|
634
|
+
if [[ -x "$executable_dir/Leither" ]]; then
|
|
635
|
+
discovered_root=$(cd "$executable_dir" && pwd -P)
|
|
636
|
+
fi
|
|
637
|
+
elif [[ -n "$leither_cwd" && -x "$leither_cwd/Leither" ]]; then
|
|
638
|
+
discovered_root=$(cd "$leither_cwd" && pwd -P)
|
|
639
|
+
fi
|
|
640
|
+
|
|
641
|
+
if [[ -n "$discovered_root" ]]; then
|
|
642
|
+
duplicate=0
|
|
643
|
+
for known_root in "${running_roots[@]:-}"; do
|
|
644
|
+
if [[ "$known_root" == "$discovered_root" ]]; then duplicate=1; break; fi
|
|
645
|
+
done
|
|
646
|
+
if (( ! duplicate )); then running_roots+=("$discovered_root"); fi
|
|
647
|
+
fi
|
|
648
|
+
done <<< "$running_pids"
|
|
649
|
+
|
|
650
|
+
if (( ${#running_roots[@]} == 0 )); then
|
|
651
|
+
echo "LePan installation stopped: Leither is running, but its root directory could not be determined." >&2
|
|
652
|
+
echo "Ensure the account running setup can inspect the Leither process." >&2
|
|
653
|
+
exit 1
|
|
654
|
+
fi
|
|
655
|
+
|
|
656
|
+
if [[ -n "$LEITHER_WORKDIR" ]]; then
|
|
657
|
+
if [[ ! -d "$LEITHER_WORKDIR" ]]; then
|
|
658
|
+
echo "LePan installation stopped: $LEITHER_WORKDIR is not a directory." >&2
|
|
659
|
+
exit 1
|
|
660
|
+
fi
|
|
661
|
+
requested_root=$(cd "$LEITHER_WORKDIR" && pwd -P)
|
|
662
|
+
root_is_running=0
|
|
663
|
+
for known_root in "${running_roots[@]}"; do
|
|
664
|
+
if [[ "$known_root" == "$requested_root" ]]; then root_is_running=1; break; fi
|
|
665
|
+
done
|
|
666
|
+
if (( ! root_is_running )); then
|
|
667
|
+
echo "LePan installation stopped: no running Leither service uses $requested_root." >&2
|
|
668
|
+
exit 1
|
|
669
|
+
fi
|
|
670
|
+
LEITHER_WORKDIR="$requested_root"
|
|
671
|
+
elif (( ${#running_roots[@]} == 1 )); then
|
|
672
|
+
LEITHER_WORKDIR="${running_roots[0]}"
|
|
673
|
+
else
|
|
674
|
+
echo "LePan installation stopped: more than one Leither service is running." >&2
|
|
675
|
+
printf ' %s\n' "${running_roots[@]}" >&2
|
|
676
|
+
echo "Rerun with --leither-root and one of the directories above." >&2
|
|
677
|
+
exit 1
|
|
678
|
+
fi
|
|
679
|
+
|
|
680
|
+
if [[ ! -x "$LEITHER_WORKDIR/Leither" ]]; then
|
|
681
|
+
echo "LePan installation stopped: the running Leither executable is not accessible at $LEITHER_WORKDIR/Leither." >&2
|
|
682
|
+
exit 1
|
|
683
|
+
fi
|
|
684
|
+
echo "Found running Leither service at $LEITHER_WORKDIR"
|
|
685
|
+
leither_version "$LEITHER_WORKDIR" >/dev/null || exit 1
|
|
686
|
+
|
|
687
|
+
if (( LEITHER_SERVICE_ONLY )); then
|
|
688
|
+
if (( ! LEITHER_SERVICE_BOOTSTRAPPED )); then configure_leither_service running; fi
|
|
689
|
+
configure_macos_leither_firewall
|
|
690
|
+
echo "Leither system service setup is complete. LePan application files were not changed."
|
|
691
|
+
exit 0
|
|
692
|
+
fi
|
|
693
|
+
|
|
694
|
+
if [[ -s "$LEITHER_WORKDIR/lifeDrive.owner" && "$(sed -n '1p' "$LEITHER_WORKDIR/lifeDrive.owner")" != "lifedrive-key-auth-v1" ]]; then
|
|
695
|
+
echo "LePan installation stopped: the existing prototype uses retired password-owner state." >&2
|
|
696
|
+
echo "This key-auth release intentionally requires a fresh LePan owner state." >&2
|
|
697
|
+
exit 1
|
|
698
|
+
fi
|
|
699
|
+
if [[ -s "$LEITHER_WORKDIR/lifeDrive.owner" && -z "$HOUSEHOLD_CONFIG" ]] && (( ! UPGRADE_ONLY && ! MANAGEMENT_ONLY )); then
|
|
700
|
+
echo "LePan installation stopped: an initialized LePan already exists at $LEITHER_WORKDIR." >&2
|
|
701
|
+
echo "Use --upgrade for application files or --add-device for another browser." >&2
|
|
702
|
+
exit 1
|
|
703
|
+
fi
|
|
704
|
+
|
|
705
|
+
if (( UPGRADE_ONLY )); then
|
|
706
|
+
if [[ ! -d "$LEITHER_WORKDIR/lifeDrive" ]] || { [[ ! -s "$LEITHER_WORKDIR/lifeDrive.owner" ]] && [[ ! -s "$LEITHER_WORKDIR/lifeDrive.households.json" ]]; }; then
|
|
707
|
+
echo "LePan upgrade stopped: no complete existing installation was found at $LEITHER_WORKDIR." >&2
|
|
708
|
+
echo "Install it first with: npx --yes @inoku/lepan@latest" >&2
|
|
709
|
+
exit 1
|
|
710
|
+
fi
|
|
711
|
+
echo "Existing LePan installation found. Its device keys, address, and drive data will be preserved."
|
|
712
|
+
fi
|
|
713
|
+
|
|
714
|
+
INSTALL_TEMP=$(mktemp -d "${TMPDIR:-/tmp}/lifedrive-install.XXXXXX")
|
|
715
|
+
cleanup() {
|
|
716
|
+
case "$INSTALL_TEMP" in
|
|
717
|
+
"${TMPDIR:-/tmp}"/lifedrive-install.*) rm -rf -- "$INSTALL_TEMP" ;;
|
|
718
|
+
esac
|
|
719
|
+
}
|
|
720
|
+
trap cleanup EXIT HUP INT TERM
|
|
721
|
+
|
|
722
|
+
if [[ "$RELEASE_BASE" == file://* ]]; then
|
|
723
|
+
echo "Loading the LePan release included with the npm package..."
|
|
724
|
+
else
|
|
725
|
+
echo "Downloading the latest LePan release from GitHub..."
|
|
726
|
+
fi
|
|
727
|
+
curl -fsSL --retry 3 --connect-timeout 20 -o "$INSTALL_TEMP/$ARCHIVE_NAME" "$RELEASE_BASE/$ARCHIVE_NAME"
|
|
728
|
+
curl -fsSL --retry 3 --connect-timeout 20 -o "$INSTALL_TEMP/$CHECKSUM_NAME" "$RELEASE_BASE/$CHECKSUM_NAME"
|
|
729
|
+
|
|
730
|
+
expected_checksum=$(awk 'NR==1 {print $1}' "$INSTALL_TEMP/$CHECKSUM_NAME")
|
|
731
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
732
|
+
actual_checksum=$(sha256sum "$INSTALL_TEMP/$ARCHIVE_NAME" | awk '{print $1}')
|
|
733
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
734
|
+
actual_checksum=$(shasum -a 256 "$INSTALL_TEMP/$ARCHIVE_NAME" | awk '{print $1}')
|
|
735
|
+
else
|
|
736
|
+
echo "LePan installation requires sha256sum or shasum." >&2
|
|
737
|
+
exit 1
|
|
738
|
+
fi
|
|
739
|
+
if [[ ! "$expected_checksum" =~ ^[a-fA-F0-9]{64}$ || "$actual_checksum" != "$expected_checksum" ]]; then
|
|
740
|
+
echo "LePan release verification failed; the downloaded bundle was not installed." >&2
|
|
741
|
+
exit 1
|
|
742
|
+
fi
|
|
743
|
+
|
|
744
|
+
mkdir -p "$INSTALL_TEMP/bundle"
|
|
745
|
+
tar -xzf "$INSTALL_TEMP/$ARCHIVE_NAME" -C "$INSTALL_TEMP/bundle"
|
|
746
|
+
if [[ ! -x "$INSTALL_TEMP/bundle/lifeDrive/lifeDrive-setup.sh" || ! -f "$INSTALL_TEMP/bundle/lifeDrive/main.go" ]]; then
|
|
747
|
+
echo "LePan release verification failed; required files are missing." >&2
|
|
748
|
+
exit 1
|
|
749
|
+
fi
|
|
750
|
+
|
|
751
|
+
if (( ! MANAGEMENT_ONLY )); then configure_macos_leither_firewall; fi
|
|
752
|
+
|
|
753
|
+
if (( MANAGEMENT_ONLY )); then
|
|
754
|
+
export LIFEDRIVE_WORKDIR="$LEITHER_WORKDIR"
|
|
755
|
+
export LIFEDRIVE_LEITHER_PATH="$LEITHER_WORKDIR/Leither"
|
|
756
|
+
management_command=(/bin/bash "$INSTALL_TEMP/bundle/lifeDrive/lifeDrive-setup.sh")
|
|
757
|
+
if (( setup_arg_count )); then management_command+=("${setup_command_args[@]}"); fi
|
|
758
|
+
"${management_command[@]}"
|
|
759
|
+
exit 0
|
|
760
|
+
fi
|
|
761
|
+
|
|
762
|
+
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
|
+
if [[ -d "$LEITHER_WORKDIR/lifeDrive" ]]; then cp -R "$LEITHER_WORKDIR/lifeDrive" "$backup_dir/"; fi
|
|
766
|
+
echo "Previous LePan files backed up to $backup_dir"
|
|
767
|
+
fi
|
|
768
|
+
|
|
769
|
+
mkdir -p "$LEITHER_WORKDIR/lifeDrive"
|
|
770
|
+
# Remove the previous top-level JavaScript MApp entries before installing the
|
|
771
|
+
# Go dispatcher. Web assets below their hashed asset directory are retained.
|
|
772
|
+
old_entry_files=(
|
|
773
|
+
access_check.js access_get_security_mode.js drive_get_state.js drive_list.js drive_open.js
|
|
774
|
+
file_get_info.js file_read.js folder_create.js health.js object_copy.js object_move.js
|
|
775
|
+
object_purge.js object_trash.js server_capabilities.js trash_list.js trash_restore.js
|
|
776
|
+
upload_abort.js upload_begin.js upload_complete.js upload_write.js access_check.test.mjs
|
|
777
|
+
lifeDrive-install-service.sh
|
|
778
|
+
)
|
|
779
|
+
for old_entry in "${old_entry_files[@]}"; do
|
|
780
|
+
old_path="$LEITHER_WORKDIR/lifeDrive/$old_entry"
|
|
781
|
+
if [[ -f "$old_path" ]]; then rm -f -- "$old_path"; fi
|
|
782
|
+
done
|
|
783
|
+
# Remove the obsolete nested web build. The Leither entry bootstrap resolves
|
|
784
|
+
# the current top-level browser objects below /mm/<app-mid>:<version>/.
|
|
785
|
+
old_web_dir="$LEITHER_WORKDIR/lifeDrive/web"
|
|
786
|
+
if [[ -d "$old_web_dir" ]]; then rm -rf -- "$old_web_dir"; fi
|
|
787
|
+
# Hosted-demo assets were accidentally included in the 0.7.8 release. They
|
|
788
|
+
# are not used by the native app and collide with Leither's MiMei short names.
|
|
789
|
+
old_demo_assets="$LEITHER_WORKDIR/lifeDrive/inoku"
|
|
790
|
+
if [[ -d "$old_demo_assets" ]]; then rm -rf -- "$old_demo_assets"; fi
|
|
791
|
+
# Tahoe V0.24.05 derives the same application-entry short name, "index", from
|
|
792
|
+
# index.html and index_entry.js. Remove the rejected TweetWeb-era filename when
|
|
793
|
+
# upgrading to the distinct top-level browser object lifedrive.js.
|
|
794
|
+
old_browser_entry="$LEITHER_WORKDIR/lifeDrive/index_entry.js"
|
|
795
|
+
if [[ -f "$old_browser_entry" ]]; then rm -f -- "$old_browser_entry"; fi
|
|
796
|
+
# CSS is embedded in index.html. An index.css object collides with index.html
|
|
797
|
+
# under the same V0.24.05 short-name normalization.
|
|
798
|
+
old_browser_style="$LEITHER_WORKDIR/lifeDrive/index.css"
|
|
799
|
+
if [[ -f "$old_browser_style" ]]; then rm -f -- "$old_browser_style"; fi
|
|
800
|
+
cp -R "$INSTALL_TEMP/bundle/lifeDrive/." "$LEITHER_WORKDIR/lifeDrive/"
|
|
801
|
+
if [[ -d "$INSTALL_TEMP/bundle/identity" ]]; then
|
|
802
|
+
mkdir -p "$LEITHER_WORKDIR/lifedrive-identity"
|
|
803
|
+
for identity_asset in "$INSTALL_TEMP/bundle/identity/"*; do
|
|
804
|
+
identity_name=$(basename "$identity_asset")
|
|
805
|
+
cp "$identity_asset" "$LEITHER_WORKDIR/lifedrive-identity/.$identity_name.new"
|
|
806
|
+
mv "$LEITHER_WORKDIR/lifedrive-identity/.$identity_name.new" "$LEITHER_WORKDIR/lifedrive-identity/$identity_name"
|
|
807
|
+
done
|
|
808
|
+
chmod 700 "$LEITHER_WORKDIR/lifedrive-identity"
|
|
809
|
+
fi
|
|
810
|
+
chmod 700 "$LEITHER_WORKDIR/lifeDrive"
|
|
811
|
+
chmod 700 "$LEITHER_WORKDIR/lifeDrive/"*.sh
|
|
812
|
+
|
|
813
|
+
export LIFEDRIVE_WORKDIR="$LEITHER_WORKDIR"
|
|
814
|
+
export LIFEDRIVE_LEITHER_PATH="$LEITHER_WORKDIR/Leither"
|
|
815
|
+
if (( HOUSEHOLD_SETUP )); then
|
|
816
|
+
/bin/bash "$LEITHER_WORKDIR/lifedrive-identity/setup-node.sh" "$LEITHER_WORKDIR"
|
|
817
|
+
exit 0
|
|
818
|
+
fi
|
|
819
|
+
if [[ -n "$HOUSEHOLD_CONFIG" ]]; then
|
|
820
|
+
/bin/bash "$LEITHER_WORKDIR/lifedrive-identity/install.sh" "$HOUSEHOLD_CONFIG"
|
|
821
|
+
exit 0
|
|
822
|
+
fi
|
|
823
|
+
if [[ -s "$LEITHER_WORKDIR/lifeDrive.households.json" ]]; then
|
|
824
|
+
/bin/bash "$LEITHER_WORKDIR/lifeDrive/lifeDrive-upgrade-household.sh"
|
|
825
|
+
exit 0
|
|
826
|
+
fi
|
|
827
|
+
setup_command=("$LEITHER_WORKDIR/lifeDrive/lifeDrive-setup.sh")
|
|
828
|
+
if (( setup_arg_count )); then
|
|
829
|
+
setup_command+=("${setup_command_args[@]}")
|
|
830
|
+
fi
|
|
831
|
+
"${setup_command[@]}"
|