@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
package/bin/lifedrive.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { spawnSync } = require("node:child_process");
|
|
7
|
+
const { pathToFileURL } = require("node:url");
|
|
8
|
+
|
|
9
|
+
const distributionDirectory = path.resolve(__dirname, "..", "dist");
|
|
10
|
+
const bundlePath = path.join(distributionDirectory, "lifedrive-bundle.tar.gz");
|
|
11
|
+
const checksumPath = `${bundlePath}.sha256`;
|
|
12
|
+
|
|
13
|
+
function windowsArguments(values) {
|
|
14
|
+
const valueOptions = new Map([
|
|
15
|
+
["--leither-root", "-LeitherRoot"],
|
|
16
|
+
["--storage-max-gb", "-StorageMaxGB"],
|
|
17
|
+
]);
|
|
18
|
+
const switches = new Map([
|
|
19
|
+
["--no-install-leither", "-NoInstallLeither"],
|
|
20
|
+
["--leither-service", "-LeitherService"],
|
|
21
|
+
["--upgrade", "-Upgrade"],
|
|
22
|
+
["--household", "-Household"],
|
|
23
|
+
["--help", "-Help"],
|
|
24
|
+
["-h", "-Help"],
|
|
25
|
+
]);
|
|
26
|
+
const translated = [];
|
|
27
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
28
|
+
const argument = values[index];
|
|
29
|
+
const equals = argument.indexOf("=");
|
|
30
|
+
const option = equals >= 0 ? argument.slice(0, equals) : argument;
|
|
31
|
+
if (valueOptions.has(option)) {
|
|
32
|
+
const value = equals >= 0 ? argument.slice(equals + 1) : values[++index];
|
|
33
|
+
if (!value || value.startsWith("-")) throw new Error(`${option} needs a value.`);
|
|
34
|
+
translated.push(valueOptions.get(option), value);
|
|
35
|
+
} else if (switches.has(argument)) {
|
|
36
|
+
translated.push(switches.get(argument));
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error(`Unsupported Windows installer option: ${argument}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return translated;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function main() {
|
|
45
|
+
if (process.platform === "win32") {
|
|
46
|
+
const installerPath = path.join(distributionDirectory, "lifedrive-install.ps1");
|
|
47
|
+
if (![installerPath, bundlePath, checksumPath].every(candidate => fs.existsSync(candidate))) {
|
|
48
|
+
throw new Error("The npm package is incomplete. Reinstall @inoku/lepan and try again.");
|
|
49
|
+
}
|
|
50
|
+
const result = spawnSync(
|
|
51
|
+
"powershell.exe",
|
|
52
|
+
["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", installerPath,
|
|
53
|
+
"-PackageDirectory", distributionDirectory, ...windowsArguments(process.argv.slice(2))],
|
|
54
|
+
{ stdio: "inherit", env: process.env }
|
|
55
|
+
);
|
|
56
|
+
if (result.error) throw result.error;
|
|
57
|
+
if (typeof result.status === "number") process.exitCode = result.status;
|
|
58
|
+
else process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (process.platform !== "linux" && process.platform !== "darwin") {
|
|
63
|
+
throw new Error("LePan setup supports native Linux, macOS, and x64 Windows Leither servers.");
|
|
64
|
+
}
|
|
65
|
+
const installerPath = path.join(distributionDirectory, "lifedrive-install.sh");
|
|
66
|
+
if (!fs.existsSync("/bin/bash")) {
|
|
67
|
+
throw new Error("LePan setup requires /bin/bash.");
|
|
68
|
+
}
|
|
69
|
+
if (![installerPath, bundlePath, checksumPath].every(candidate => fs.existsSync(candidate))) {
|
|
70
|
+
throw new Error("The npm package is incomplete. Reinstall @inoku/lepan and try again.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const localReleaseBase = pathToFileURL(distributionDirectory).href.replace(/\/$/, "");
|
|
74
|
+
const result = spawnSync(
|
|
75
|
+
"/bin/bash",
|
|
76
|
+
[installerPath, "--release-base", localReleaseBase, ...process.argv.slice(2)],
|
|
77
|
+
{ stdio: "inherit", env: process.env }
|
|
78
|
+
);
|
|
79
|
+
if (result.error) throw result.error;
|
|
80
|
+
if (typeof result.status === "number") process.exitCode = result.status;
|
|
81
|
+
else if (result.signal === "SIGINT") process.exitCode = 130;
|
|
82
|
+
else if (result.signal === "SIGTERM") process.exitCode = 143;
|
|
83
|
+
else process.exitCode = 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
main();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
process.stderr.write(`LePan installer stopped: ${error.message}\n`);
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
05320977b19562ac0e8f1419597dd92def37f0f13ec85767e23514cfc8235e53 lifedrive-bundle.tar.gz
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[Parameter(Mandatory = $true)]
|
|
4
|
+
[string]$PackageDirectory,
|
|
5
|
+
[string]$LeitherRoot,
|
|
6
|
+
[Int64]$StorageMaxGB = 0,
|
|
7
|
+
[switch]$NoInstallLeither,
|
|
8
|
+
[switch]$LeitherService,
|
|
9
|
+
[switch]$Upgrade,
|
|
10
|
+
[switch]$Household,
|
|
11
|
+
[switch]$Help
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
$ErrorActionPreference = "Stop"
|
|
15
|
+
$leitherTaskName = "LifeDrive Leither"
|
|
16
|
+
$identityTaskName = "LifeDrive Identity"
|
|
17
|
+
$defaultStorageGB = 100
|
|
18
|
+
$releaseBase = "http://vzhan.cn/mm/Fc1BRTFafOGzq5P8KmkVJqwS2v2"
|
|
19
|
+
|
|
20
|
+
function Show-Usage {
|
|
21
|
+
Write-Host @"
|
|
22
|
+
Usage: npx --yes @inoku/lepan [options]
|
|
23
|
+
|
|
24
|
+
Windows options:
|
|
25
|
+
--leither-root DIR Existing node or directory for a new node.
|
|
26
|
+
--storage-max-gb GB New node storage maximum (default: 100 GB).
|
|
27
|
+
--no-install-leither Require an already running Leither node.
|
|
28
|
+
--leither-service Configure Leither startup only.
|
|
29
|
+
--upgrade Upgrade an existing Windows LePan installation.
|
|
30
|
+
--household Accepted for parity; Windows uses household mode by default.
|
|
31
|
+
|
|
32
|
+
Run Windows Terminal or PowerShell as Administrator. Windows setup supports
|
|
33
|
+
native x64 Windows 10/11 and Windows Server; WSL and Git Bash are not required.
|
|
34
|
+
"@
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if ($Help) { Show-Usage; return }
|
|
38
|
+
if ($Upgrade -and $LeitherService) { throw "Use -Upgrade and -LeitherService separately." }
|
|
39
|
+
if ($StorageMaxGB -lt 0) { throw "Leither storage must be a positive whole number of gigabytes." }
|
|
40
|
+
if ($StorageMaxGB -eq 0 -and $env:LIFEDRIVE_STORAGE_MAX_GB) {
|
|
41
|
+
$parsedStorage = 0L
|
|
42
|
+
if (-not [Int64]::TryParse($env:LIFEDRIVE_STORAGE_MAX_GB, [ref]$parsedStorage) -or $parsedStorage -lt 1) {
|
|
43
|
+
throw "LIFEDRIVE_STORAGE_MAX_GB must be a positive whole number."
|
|
44
|
+
}
|
|
45
|
+
$StorageMaxGB = $parsedStorage
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
$principal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent())
|
|
49
|
+
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
50
|
+
throw "Run Windows Terminal or PowerShell as Administrator, then rerun the npx command."
|
|
51
|
+
}
|
|
52
|
+
if (-not [Environment]::Is64BitOperatingSystem) {
|
|
53
|
+
throw "The official Leither Windows build requires 64-bit Windows."
|
|
54
|
+
}
|
|
55
|
+
$architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
|
|
56
|
+
if ($architecture -ne "AMD64") {
|
|
57
|
+
throw "The official Leither Windows package currently supports x64 (AMD64) only."
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Get-LeitherVersion {
|
|
61
|
+
param([string]$Root)
|
|
62
|
+
$executable = Join-Path $Root "Leither.exe"
|
|
63
|
+
Push-Location $Root
|
|
64
|
+
try {
|
|
65
|
+
$reply = & $executable version --json 2>$null
|
|
66
|
+
if ($LASTEXITCODE -ne 0) { throw "Leither version command failed." }
|
|
67
|
+
} finally {
|
|
68
|
+
Pop-Location
|
|
69
|
+
}
|
|
70
|
+
$parsed = ($reply -join [Environment]::NewLine) | ConvertFrom-Json
|
|
71
|
+
$reported = if ($parsed.data.version) { [string]$parsed.data.version } else { [string]$parsed.version }
|
|
72
|
+
$normalized = $reported.TrimStart("V")
|
|
73
|
+
$version = $null
|
|
74
|
+
if (-not [Version]::TryParse($normalized, [ref]$version) -or $version -lt [Version]"0.24.11") {
|
|
75
|
+
throw "LePan requires Leither V0.24.11 or newer. Existing binaries are never replaced."
|
|
76
|
+
}
|
|
77
|
+
return $version
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function Get-LeitherPort {
|
|
81
|
+
param([string]$Root)
|
|
82
|
+
$path = @("SystemVars.json", "systemvars.json") | ForEach-Object { Join-Path $Root $_ } | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
|
|
83
|
+
if (-not $path) { return 4800 }
|
|
84
|
+
$value = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
|
|
85
|
+
$port = if ($null -ne $value.ServicePort) { [int]$value.ServicePort } else { 4800 }
|
|
86
|
+
if ($port -lt 1 -or $port -gt 65535) { throw "Invalid Leither ServicePort in $path." }
|
|
87
|
+
return $port
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Get-RunningLeitherRoots {
|
|
91
|
+
$roots = @()
|
|
92
|
+
Get-CimInstance Win32_Process -Filter "Name='Leither.exe'" -ErrorAction SilentlyContinue | ForEach-Object {
|
|
93
|
+
if ($_.ExecutablePath) {
|
|
94
|
+
$candidate = [IO.Path]::GetFullPath((Split-Path -Parent $_.ExecutablePath))
|
|
95
|
+
if ($roots -notcontains $candidate) { $roots += $candidate }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return $roots
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function Protect-NewNodeDirectory {
|
|
102
|
+
param([string]$Path)
|
|
103
|
+
$acl = [Security.AccessControl.DirectorySecurity]::new()
|
|
104
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
105
|
+
$inheritance = [Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit"
|
|
106
|
+
$propagation = [Security.AccessControl.PropagationFlags]::None
|
|
107
|
+
foreach ($sidText in @("S-1-5-18", "S-1-5-32-544", [Security.Principal.WindowsIdentity]::GetCurrent().User.Value)) {
|
|
108
|
+
$sid = [Security.Principal.SecurityIdentifier]::new($sidText)
|
|
109
|
+
$rule = [Security.AccessControl.FileSystemAccessRule]::new(
|
|
110
|
+
$sid, [Security.AccessControl.FileSystemRights]::FullControl, $inheritance,
|
|
111
|
+
$propagation, [Security.AccessControl.AccessControlType]::Allow)
|
|
112
|
+
$acl.AddAccessRule($rule)
|
|
113
|
+
}
|
|
114
|
+
Set-Acl -LiteralPath $Path -AclObject $acl
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function Grant-SystemTaskAccess {
|
|
118
|
+
param([string]$Path)
|
|
119
|
+
$acl = Get-Acl -LiteralPath $Path
|
|
120
|
+
$inheritance = [Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit"
|
|
121
|
+
$sid = [Security.Principal.SecurityIdentifier]::new("S-1-5-18")
|
|
122
|
+
$rule = [Security.AccessControl.FileSystemAccessRule]::new(
|
|
123
|
+
$sid, [Security.AccessControl.FileSystemRights]::FullControl, $inheritance,
|
|
124
|
+
[Security.AccessControl.PropagationFlags]::None, [Security.AccessControl.AccessControlType]::Allow)
|
|
125
|
+
$acl.SetAccessRule($rule)
|
|
126
|
+
Set-Acl -LiteralPath $Path -AclObject $acl
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function Assert-PortAvailable {
|
|
130
|
+
param([int]$Port)
|
|
131
|
+
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $Port)
|
|
132
|
+
try {
|
|
133
|
+
$listener.Start()
|
|
134
|
+
} catch {
|
|
135
|
+
throw "Leither port $Port is unavailable. Free it or configure a different ServicePort in the selected node's SystemVars.json."
|
|
136
|
+
} finally {
|
|
137
|
+
$listener.Stop()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function Install-LeitherBinary {
|
|
142
|
+
param([string]$Root)
|
|
143
|
+
$work = Join-Path ([IO.Path]::GetTempPath()) ("lifedrive-leither-" + [Guid]::NewGuid().ToString("N"))
|
|
144
|
+
New-Item -ItemType Directory -Path $work | Out-Null
|
|
145
|
+
try {
|
|
146
|
+
Write-Host "Finding the latest official Leither Windows release..."
|
|
147
|
+
$index = (Invoke-WebRequest -UseBasicParsing -Uri ($releaseBase + "/")).Content
|
|
148
|
+
$versions = [regex]::Matches($index, 'href="V(\d+\.\d+\.\d+)/"') | ForEach-Object { [Version]$_.Groups[1].Value } | Sort-Object
|
|
149
|
+
if (-not $versions) { throw "No Leither release was found in the official index." }
|
|
150
|
+
$latest = "V" + $versions[-1].ToString()
|
|
151
|
+
$artifact = "Leither.windows.amd64"
|
|
152
|
+
$binaryURL = "$releaseBase/$latest/$artifact"
|
|
153
|
+
$downloaded = Join-Path $work "Leither.exe"
|
|
154
|
+
$checksum = Join-Path $work "Leither.sha256"
|
|
155
|
+
Write-Host "Downloading Leither $latest for Windows x64..."
|
|
156
|
+
Invoke-WebRequest -UseBasicParsing -Uri $binaryURL -OutFile $downloaded
|
|
157
|
+
Invoke-WebRequest -UseBasicParsing -Uri ($binaryURL + ".sha256") -OutFile $checksum
|
|
158
|
+
$expected = ((Get-Content -LiteralPath $checksum -Raw).Trim() -split "\s+")[0].ToLowerInvariant()
|
|
159
|
+
$actual = (Get-FileHash -LiteralPath $downloaded -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
160
|
+
if ($expected -notmatch '^[a-f0-9]{64}$' -or $actual -ne $expected) {
|
|
161
|
+
throw "Leither SHA-256 verification failed; the binary was not installed."
|
|
162
|
+
}
|
|
163
|
+
$destination = Join-Path $Root "Leither.exe"
|
|
164
|
+
if (Test-Path -LiteralPath $destination) { throw "Leither.exe already exists and was not replaced." }
|
|
165
|
+
Move-Item -LiteralPath $downloaded -Destination $destination
|
|
166
|
+
} finally {
|
|
167
|
+
Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function Set-NewNodeStorage {
|
|
172
|
+
param([string]$Root, [Int64]$RequestedGB)
|
|
173
|
+
$config = Join-Path $Root "ds\config"
|
|
174
|
+
if (-not (Test-Path -LiteralPath $config -PathType Leaf)) {
|
|
175
|
+
throw "Leither initialization did not create $config."
|
|
176
|
+
}
|
|
177
|
+
$driveRoot = [IO.Path]::GetPathRoot($Root)
|
|
178
|
+
$drive = [IO.DriveInfo]::new($driveRoot)
|
|
179
|
+
$availableGB = [Math]::Floor($drive.AvailableFreeSpace / 1000000000)
|
|
180
|
+
Write-Host ""
|
|
181
|
+
Write-Host "Choose the maximum hard-drive space Leither may use for LePan and other data on this node."
|
|
182
|
+
Write-Host "Available now on the selected drive: $availableGB GB."
|
|
183
|
+
$selected = $RequestedGB
|
|
184
|
+
if ($selected -eq 0) {
|
|
185
|
+
if ([Console]::IsInputRedirected) {
|
|
186
|
+
$selected = $defaultStorageGB
|
|
187
|
+
Write-Host "No interactive input was detected; using the $defaultStorageGB GB default."
|
|
188
|
+
} else {
|
|
189
|
+
while ($true) {
|
|
190
|
+
$answer = Read-Host "Maximum Leither storage in GB [$defaultStorageGB]"
|
|
191
|
+
if ([string]::IsNullOrWhiteSpace($answer)) { $selected = $defaultStorageGB }
|
|
192
|
+
else {
|
|
193
|
+
$candidate = 0L
|
|
194
|
+
if ([Int64]::TryParse($answer, [ref]$candidate) -and $candidate -gt 0) { $selected = $candidate }
|
|
195
|
+
else { Write-Warning "Enter a positive whole number of gigabytes."; continue }
|
|
196
|
+
}
|
|
197
|
+
if ($selected -gt $availableGB) { Write-Warning "That exceeds the $availableGB GB currently available."; continue }
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if ($selected -lt 1) { throw "Leither storage must be a positive whole number of gigabytes." }
|
|
203
|
+
if ($selected -gt $availableGB) { throw "The selected limit ($selected GB) exceeds the $availableGB GB currently available." }
|
|
204
|
+
|
|
205
|
+
$source = [IO.File]::ReadAllText($config)
|
|
206
|
+
$matches = [regex]::Matches($source, '"StorageMax"\s*:\s*"([^"]*)"')
|
|
207
|
+
if ($matches.Count -ne 1) { throw "Leither datastore StorageMax must occur exactly once." }
|
|
208
|
+
$desired = [string]$selected + "GB"
|
|
209
|
+
$value = $matches[0].Groups[1]
|
|
210
|
+
$next = $source.Substring(0, $value.Index) + $desired + $source.Substring($value.Index + $value.Length)
|
|
211
|
+
$verified = $next | ConvertFrom-Json
|
|
212
|
+
if ([string]$verified.Datastore.StorageMax -ne $desired) { throw "Updated Leither storage configuration did not validate." }
|
|
213
|
+
$temporary = $config + ".lifedrive-install-" + [Guid]::NewGuid().ToString("N")
|
|
214
|
+
$backup = $config + ".lifedrive-install-backup-" + [Guid]::NewGuid().ToString("N")
|
|
215
|
+
[IO.File]::WriteAllText($temporary, $next, [Text.UTF8Encoding]::new($false))
|
|
216
|
+
try {
|
|
217
|
+
[IO.File]::Replace($temporary, $config, $backup, $true)
|
|
218
|
+
Remove-Item -LiteralPath $backup -Force -ErrorAction SilentlyContinue
|
|
219
|
+
} catch {
|
|
220
|
+
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
|
221
|
+
throw
|
|
222
|
+
}
|
|
223
|
+
Write-Host "Leither storage limit saved as $selected GB."
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function Test-ManagedLeitherTask {
|
|
227
|
+
param($Task, [string]$Root)
|
|
228
|
+
if (-not $Task -or $Task.Actions.Count -ne 1) { return $false }
|
|
229
|
+
$action = $Task.Actions[0]
|
|
230
|
+
$expected = Join-Path $Root "Leither.exe"
|
|
231
|
+
return ([IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($action.Execute)) -eq [IO.Path]::GetFullPath($expected)) -and
|
|
232
|
+
($action.Arguments -eq "run") -and
|
|
233
|
+
([IO.Path]::GetFullPath($action.WorkingDirectory) -eq [IO.Path]::GetFullPath($Root))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function Register-LeitherTask {
|
|
237
|
+
param([string]$Root, [bool]$StartNow)
|
|
238
|
+
$existing = Get-ScheduledTask -TaskName $leitherTaskName -ErrorAction SilentlyContinue
|
|
239
|
+
if ($existing -and -not (Test-ManagedLeitherTask $existing $Root)) {
|
|
240
|
+
throw "A different scheduled task named '$leitherTaskName' already exists. It was not replaced."
|
|
241
|
+
}
|
|
242
|
+
$action = New-ScheduledTaskAction -Execute (Join-Path $Root "Leither.exe") -Argument "run" -WorkingDirectory $Root
|
|
243
|
+
$trigger = New-ScheduledTaskTrigger -AtStartup
|
|
244
|
+
$taskPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
|
245
|
+
$settingsParameters = @{
|
|
246
|
+
AllowStartIfOnBatteries = $true
|
|
247
|
+
DontStopIfGoingOnBatteries = $true
|
|
248
|
+
ExecutionTimeLimit = [TimeSpan]::Zero
|
|
249
|
+
RestartCount = 999
|
|
250
|
+
RestartInterval = (New-TimeSpan -Minutes 1)
|
|
251
|
+
StartWhenAvailable = $true
|
|
252
|
+
MultipleInstances = "IgnoreNew"
|
|
253
|
+
}
|
|
254
|
+
$settings = New-ScheduledTaskSettingsSet @settingsParameters
|
|
255
|
+
$definition = New-ScheduledTask -Action $action -Trigger $trigger -Principal $taskPrincipal -Settings $settings -Description "Leither node installed by LePan"
|
|
256
|
+
Register-ScheduledTask -TaskName $leitherTaskName -InputObject $definition -Force | Out-Null
|
|
257
|
+
if ($StartNow -and (Get-ScheduledTask -TaskName $leitherTaskName).State -ne "Running") {
|
|
258
|
+
Start-ScheduledTask -TaskName $leitherTaskName
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function Ensure-LeitherFirewall {
|
|
263
|
+
param([string]$Root, [int]$Port)
|
|
264
|
+
$name = "LifeDrive-Leither"
|
|
265
|
+
$program = Join-Path $Root "Leither.exe"
|
|
266
|
+
$rule = Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue
|
|
267
|
+
if ($rule) {
|
|
268
|
+
$application = $rule | Get-NetFirewallApplicationFilter
|
|
269
|
+
$portFilter = $rule | Get-NetFirewallPortFilter
|
|
270
|
+
if ([IO.Path]::GetFullPath($application.Program) -ne [IO.Path]::GetFullPath($program) -or [string]$portFilter.LocalPort -ne [string]$Port -or $portFilter.Protocol -ne "TCP") {
|
|
271
|
+
throw "An existing firewall rule named $name has a different program or port. It was not replaced."
|
|
272
|
+
}
|
|
273
|
+
Enable-NetFirewallRule -Name $name | Out-Null
|
|
274
|
+
} else {
|
|
275
|
+
New-NetFirewallRule -Name $name -DisplayName "LePan Leither node" -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol TCP -LocalPort $Port -Program $program | Out-Null
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function Wait-Leither {
|
|
280
|
+
param([string]$Root, [Version]$Version, [int]$Port)
|
|
281
|
+
for ($attempt = 0; $attempt -lt 45; $attempt++) {
|
|
282
|
+
try {
|
|
283
|
+
$reply = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/getvar?name=ver" -TimeoutSec 2
|
|
284
|
+
$reported = ([string]$reply).TrimStart("V")
|
|
285
|
+
$process = Get-CimInstance Win32_Process -Filter "Name='Leither.exe'" -ErrorAction SilentlyContinue | Where-Object {
|
|
286
|
+
$_.ExecutablePath -and [IO.Path]::GetFullPath($_.ExecutablePath) -eq [IO.Path]::GetFullPath((Join-Path $Root "Leither.exe"))
|
|
287
|
+
}
|
|
288
|
+
if ($process -and $reported -eq $Version.ToString()) { return }
|
|
289
|
+
} catch {}
|
|
290
|
+
Start-Sleep -Seconds 1
|
|
291
|
+
}
|
|
292
|
+
throw "Leither did not become ready. Inspect the '$leitherTaskName' task history."
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
$packageRoot = [IO.Path]::GetFullPath($PackageDirectory)
|
|
296
|
+
$archive = Join-Path $packageRoot "lifedrive-bundle.tar.gz"
|
|
297
|
+
$checksumFile = $archive + ".sha256"
|
|
298
|
+
if (-not (Test-Path -LiteralPath $archive -PathType Leaf) -or -not (Test-Path -LiteralPath $checksumFile -PathType Leaf)) {
|
|
299
|
+
throw "The npm package is incomplete. Reinstall @inoku/lepan and try again."
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
$runningRoots = @(Get-RunningLeitherRoots)
|
|
303
|
+
if ($LeitherRoot) {
|
|
304
|
+
$root = [IO.Path]::GetFullPath($LeitherRoot)
|
|
305
|
+
} elseif ($runningRoots.Count -eq 1) {
|
|
306
|
+
$root = $runningRoots[0]
|
|
307
|
+
} elseif ($runningRoots.Count -gt 1) {
|
|
308
|
+
throw "More than one Leither process is running. Rerun with --leither-root."
|
|
309
|
+
} else {
|
|
310
|
+
$root = Join-Path $env:ProgramData "LifeDrive\Leither"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
$binary = Join-Path $root "Leither.exe"
|
|
314
|
+
$pending = Join-Path $root ".lifedrive-storage-pending"
|
|
315
|
+
$newNode = -not (Test-Path -LiteralPath $binary -PathType Leaf)
|
|
316
|
+
if (Test-Path -LiteralPath $pending -PathType Leaf) { $newNode = $true }
|
|
317
|
+
|
|
318
|
+
if ($newNode) {
|
|
319
|
+
if ($NoInstallLeither -or $Upgrade) { throw "This operation requires an already running Leither node." }
|
|
320
|
+
if (-not (Test-Path -LiteralPath $root)) {
|
|
321
|
+
New-Item -ItemType Directory -Path $root -Force | Out-Null
|
|
322
|
+
Protect-NewNodeDirectory $root
|
|
323
|
+
}
|
|
324
|
+
$contents = @(Get-ChildItem -LiteralPath $root -Force)
|
|
325
|
+
$allowedRetry = @(".lifedrive-storage-pending", "Leither.exe", "SystemVars.json", "systemvars.json", "hostkey.cfg", "ds")
|
|
326
|
+
if (($contents | Where-Object { $allowedRetry -notcontains $_.Name }).Count -gt 0) {
|
|
327
|
+
throw "Leither is missing, but $root contains other files. Choose an empty --leither-root; nothing was replaced."
|
|
328
|
+
}
|
|
329
|
+
if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { Install-LeitherBinary $root }
|
|
330
|
+
if (-not (Test-Path -LiteralPath $pending)) { New-Item -ItemType File -Path $pending | Out-Null }
|
|
331
|
+
if (-not (Test-Path -LiteralPath (Join-Path $root "SystemVars.json")) -and -not (Test-Path -LiteralPath (Join-Path $root "systemvars.json"))) {
|
|
332
|
+
Write-Host "Initializing the new private Leither node at $root..."
|
|
333
|
+
Push-Location $root
|
|
334
|
+
try {
|
|
335
|
+
& $binary init
|
|
336
|
+
if ($LASTEXITCODE -ne 0) { throw "Leither initialization failed; its files were retained at $root." }
|
|
337
|
+
} finally {
|
|
338
|
+
Pop-Location
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
$vars = @("SystemVars.json", "systemvars.json") | ForEach-Object { Join-Path $root $_ } | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
|
|
342
|
+
if (-not $vars -or -not (Test-Path -LiteralPath (Join-Path $root "hostkey.cfg") -PathType Leaf) -or
|
|
343
|
+
-not (Test-Path -LiteralPath (Join-Path $root "ds\config") -PathType Leaf)) {
|
|
344
|
+
throw "Leither initialization is incomplete. Its files were retained at $root for inspection."
|
|
345
|
+
}
|
|
346
|
+
Set-NewNodeStorage $root $StorageMaxGB
|
|
347
|
+
Remove-Item -LiteralPath $pending -Force
|
|
348
|
+
} elseif ($StorageMaxGB -gt 0) {
|
|
349
|
+
throw "--storage-max-gb is only valid when this installer creates a new Leither node. Change an existing limit from LePan Settings."
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
$version = Get-LeitherVersion $root
|
|
353
|
+
$port = Get-LeitherPort $root
|
|
354
|
+
$runningAtRoot = $runningRoots -contains $root
|
|
355
|
+
if ($NoInstallLeither -and -not $runningAtRoot) { throw "--no-install-leither requires the selected node to be running." }
|
|
356
|
+
if ($Upgrade -and -not $runningAtRoot) { throw "--upgrade requires the selected Leither node to be running." }
|
|
357
|
+
if (-not $NoInstallLeither -and -not $Upgrade) {
|
|
358
|
+
Grant-SystemTaskAccess $root
|
|
359
|
+
if (-not $runningAtRoot) { Assert-PortAvailable $port }
|
|
360
|
+
Register-LeitherTask $root (-not $runningAtRoot)
|
|
361
|
+
}
|
|
362
|
+
Ensure-LeitherFirewall $root $port
|
|
363
|
+
if (-not $runningAtRoot) { Wait-Leither $root $version $port }
|
|
364
|
+
Write-Host "Leither V$version is ready at $root on port $port."
|
|
365
|
+
|
|
366
|
+
if ($LeitherService) {
|
|
367
|
+
Write-Host "Leither startup and firewall configuration are complete. LePan files were not changed."
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# The identity task also runs as SYSTEM, including when Leither itself is
|
|
372
|
+
# managed separately through --no-install-leither.
|
|
373
|
+
Grant-SystemTaskAccess $root
|
|
374
|
+
|
|
375
|
+
$config = Join-Path $root ".lifedrive-household\identity.json"
|
|
376
|
+
$routes = Join-Path $root "lifeDrive.households.json"
|
|
377
|
+
if ($Upgrade) {
|
|
378
|
+
if (-not (Test-Path -LiteralPath $config -PathType Leaf)) {
|
|
379
|
+
throw "LePan upgrade stopped: no Windows household installation was found at $root."
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
if (Test-Path -LiteralPath (Join-Path $root "lifeDrive.owner") -PathType Leaf) {
|
|
383
|
+
throw "This node uses the legacy browser-owner setup, which Windows does not migrate automatically. Its files were left unchanged."
|
|
384
|
+
}
|
|
385
|
+
if (Test-Path -LiteralPath $routes -PathType Leaf) {
|
|
386
|
+
throw "An initialized LePan household already exists. Rerun with --upgrade."
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
$expectedChecksum = ((Get-Content -LiteralPath $checksumFile -Raw).Trim() -split "\s+")[0].ToLowerInvariant()
|
|
391
|
+
$actualChecksum = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
392
|
+
if ($expectedChecksum -notmatch '^[a-f0-9]{64}$' -or $actualChecksum -ne $expectedChecksum) {
|
|
393
|
+
throw "LePan release verification failed; the package was not installed."
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
$installTemp = Join-Path ([IO.Path]::GetTempPath()) ("lifedrive-install-" + [Guid]::NewGuid().ToString("N"))
|
|
397
|
+
$extract = Join-Path $installTemp "bundle"
|
|
398
|
+
New-Item -ItemType Directory -Path $extract -Force | Out-Null
|
|
399
|
+
$identityTaskStopped = $false
|
|
400
|
+
try {
|
|
401
|
+
& tar.exe -xzf $archive -C $extract
|
|
402
|
+
if ($LASTEXITCODE -ne 0) { throw "LePan release archive could not be extracted." }
|
|
403
|
+
$sourceApp = Join-Path $extract "lifeDrive"
|
|
404
|
+
$sourceIdentity = Join-Path $extract "identity"
|
|
405
|
+
if (-not (Test-Path -LiteralPath (Join-Path $sourceApp "main.go") -PathType Leaf) -or
|
|
406
|
+
-not (Test-Path -LiteralPath (Join-Path $sourceIdentity "lifedrive-identity-windows-amd64.exe") -PathType Leaf) -or
|
|
407
|
+
-not (Test-Path -LiteralPath (Join-Path $sourceIdentity "setup-node.ps1") -PathType Leaf)) {
|
|
408
|
+
throw "LePan release verification failed; required Windows files are missing."
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
$identityTask = Get-ScheduledTask -TaskName $identityTaskName -ErrorAction SilentlyContinue
|
|
412
|
+
$installedIdentity = Join-Path $root "lifedrive-identity\lifedrive-identity-windows-amd64.exe"
|
|
413
|
+
if ($identityTask) {
|
|
414
|
+
if ($identityTask.Actions.Count -ne 1 -or [IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($identityTask.Actions[0].Execute)) -ne [IO.Path]::GetFullPath($installedIdentity)) {
|
|
415
|
+
throw "A different scheduled task named '$identityTaskName' already exists. It was not stopped."
|
|
416
|
+
}
|
|
417
|
+
if ($identityTask.State -eq "Running") {
|
|
418
|
+
Stop-ScheduledTask -TaskName $identityTaskName
|
|
419
|
+
$identityTaskStopped = $true
|
|
420
|
+
for ($attempt = 0; $attempt -lt 30; $attempt++) {
|
|
421
|
+
if ((Get-ScheduledTask -TaskName $identityTaskName).State -ne "Running") { break }
|
|
422
|
+
Start-Sleep -Seconds 1
|
|
423
|
+
}
|
|
424
|
+
if ((Get-ScheduledTask -TaskName $identityTaskName).State -eq "Running") {
|
|
425
|
+
throw "The existing '$identityTaskName' task did not stop; installed files were not changed."
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
$destinationApp = Join-Path $root "lifeDrive"
|
|
431
|
+
if (Test-Path -LiteralPath $destinationApp -PathType Container) {
|
|
432
|
+
$backupRoot = Join-Path $root ("deploy-backups\lifedrive-" + (Get-Date).ToUniversalTime().ToString("yyyyMMddTHHmmssZ"))
|
|
433
|
+
New-Item -ItemType Directory -Path $backupRoot -Force | Out-Null
|
|
434
|
+
Copy-Item -LiteralPath $destinationApp -Destination $backupRoot -Recurse
|
|
435
|
+
Write-Host "Previous LePan files backed up to $backupRoot"
|
|
436
|
+
}
|
|
437
|
+
New-Item -ItemType Directory -Path $destinationApp -Force | Out-Null
|
|
438
|
+
foreach ($obsolete in @("web", "inoku", "index_entry.js", "index.css", "lifeDrive-install-service.sh")) {
|
|
439
|
+
Remove-Item -LiteralPath (Join-Path $destinationApp $obsolete) -Recurse -Force -ErrorAction SilentlyContinue
|
|
440
|
+
}
|
|
441
|
+
Get-ChildItem -LiteralPath $sourceApp -Force | ForEach-Object {
|
|
442
|
+
Copy-Item -LiteralPath $_.FullName -Destination $destinationApp -Recurse -Force
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
$destinationIdentity = Join-Path $root "lifedrive-identity"
|
|
446
|
+
New-Item -ItemType Directory -Path $destinationIdentity -Force | Out-Null
|
|
447
|
+
Get-ChildItem -LiteralPath $sourceIdentity -Force | ForEach-Object {
|
|
448
|
+
Copy-Item -LiteralPath $_.FullName -Destination $destinationIdentity -Recurse -Force
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
if ($identityTaskStopped) {
|
|
452
|
+
try { Start-ScheduledTask -TaskName $identityTaskName } catch {}
|
|
453
|
+
}
|
|
454
|
+
throw
|
|
455
|
+
} finally {
|
|
456
|
+
Remove-Item -LiteralPath $installTemp -Recurse -Force -ErrorAction SilentlyContinue
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if ($Upgrade) {
|
|
460
|
+
try {
|
|
461
|
+
& (Join-Path $root "lifedrive-identity\setup-node.ps1") -LeitherRoot $root -Upgrade
|
|
462
|
+
} catch {
|
|
463
|
+
if ($identityTaskStopped) {
|
|
464
|
+
try { Start-ScheduledTask -TaskName $identityTaskName } catch {}
|
|
465
|
+
}
|
|
466
|
+
throw
|
|
467
|
+
}
|
|
468
|
+
Write-Host "LePan Windows upgrade complete. Leither was not restarted."
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
& (Join-Path $root "lifedrive-identity\setup-node.ps1") -LeitherRoot $root
|
|
473
|
+
} catch {
|
|
474
|
+
if ($identityTaskStopped) {
|
|
475
|
+
try { Start-ScheduledTask -TaskName $identityTaskName } catch {}
|
|
476
|
+
}
|
|
477
|
+
throw
|
|
478
|
+
}
|