-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkoreader-sync.ps1
More file actions
351 lines (301 loc) · 14.7 KB
/
Copy pathkoreader-sync.ps1
File metadata and controls
351 lines (301 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
<#
.SYNOPSIS
KOReader Sync — Windows Server Deployment Script
.DESCRIPTION
Installs or updates KOReader Sync as a Windows service.
Automatically downloads the latest release from GitHub.
All files are stored relative to the directory this script is run from.
On first run: downloads and installs as a Windows service.
On subsequent runs: detects the installed version and applies updates.
NSSM (Non-Sucking Service Manager) is downloaded automatically if needed.
.PARAMETER Port
Port the sync server listens on. Default: 7300
.PARAMETER DataDir
Directory for reading progress data. Default: .\data (next to this script)
.PARAMETER ServiceName
Windows service name. Default: KOReaderSync
.PARAMETER GitHubRepo
GitHub repository owner/repo. Change this if you are self-hosting a fork.
Default: your-username/koreader-sync
.PARAMETER Uninstall
Stops and removes the Windows service and NSSM, but leaves data intact.
.EXAMPLE
.\koreader-sync.ps1
.\koreader-sync.ps1 -Port 8080
.\koreader-sync.ps1 -Uninstall
#>
[CmdletBinding()]
param(
[int] $Port = 7300,
[string] $DataDir = "",
[string] $ServiceName = "KOReaderSync",
[string] $GitHubRepo = "FoxWilder/Koreader-Sync-View",
[switch] $Uninstall
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# ── Paths (all relative to where the script lives) ──────────────────────────
$Base = $PSScriptRoot
$AppDir = Join-Path $Base "app"
$NssmExe = Join-Path $Base "nssm.exe"
$VersionFile = Join-Path $Base ".installed-version"
if (-not $DataDir) { $DataDir = Join-Path $Base "data" }
# ── Helpers ─────────────────────────────────────────────────────────────────
function Write-Header([string]$text) {
Write-Host ""
Write-Host " ═══════════════════════════════════════" -ForegroundColor Cyan
Write-Host " $text" -ForegroundColor Cyan
Write-Host " ═══════════════════════════════════════" -ForegroundColor Cyan
Write-Host ""
}
function Write-Step([string]$text) {
Write-Host " ▶ $text" -ForegroundColor Yellow
}
function Write-Ok([string]$text) {
Write-Host " ✔ $text" -ForegroundColor Green
}
function Write-Err([string]$text) {
Write-Host " ✖ $text" -ForegroundColor Red
}
function Write-Info([string]$text) {
Write-Host " · $text" -ForegroundColor Gray
}
# ── Admin check ──────────────────────────────────────────────────────────────
function Assert-Admin {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$p = New-Object Security.Principal.WindowsPrincipal($id)
if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Err "This script must be run as Administrator."
Write-Info "Right-click PowerShell and choose 'Run as administrator', then try again."
exit 1
}
}
# ── Node.js check ────────────────────────────────────────────────────────────
function Assert-NodeJs {
Write-Step "Checking Node.js..."
try {
$ver = & node --version 2>&1
if ($LASTEXITCODE -ne 0) { throw "node exited $LASTEXITCODE" }
# Require v20 or higher
$major = [int]($ver -replace "v(\d+)\..*", '$1')
if ($major -lt 20) {
Write-Err "Node.js $ver is too old. Version 20 or higher required."
Write-Info "Download from: https://nodejs.org/en/download/"
exit 1
}
Write-Ok "Node.js $ver"
} catch {
Write-Err "Node.js not found."
Write-Info "Install Node.js 20+ from: https://nodejs.org/en/download/"
Write-Info "Then re-run this script."
exit 1
}
}
# ── NSSM ─────────────────────────────────────────────────────────────────────
function Get-Nssm {
if (Test-Path $NssmExe) {
Write-Ok "NSSM already present."
return
}
Write-Step "Downloading NSSM (service manager)..."
$nssmZip = Join-Path $env:TEMP "nssm.zip"
$nssmTmp = Join-Path $env:TEMP "nssm-extract"
try {
Invoke-WebRequest -Uri "https://nssm.cc/release/nssm-2.24.zip" `
-OutFile $nssmZip -UseBasicParsing
Expand-Archive -Path $nssmZip -DestinationPath $nssmTmp -Force
# NSSM zip has a subdirectory; find the right exe for this OS architecture
$arch = if ([Environment]::Is64BitOperatingSystem) { "win64" } else { "win32" }
$exe = Get-ChildItem -Path $nssmTmp -Recurse -Filter "nssm.exe" |
Where-Object { $_.Directory.Name -eq $arch } |
Select-Object -First 1
if (-not $exe) {
# Fall back to any nssm.exe in the zip
$exe = Get-ChildItem -Path $nssmTmp -Recurse -Filter "nssm.exe" |
Select-Object -First 1
}
Copy-Item $exe.FullName $NssmExe
Remove-Item $nssmZip -Force -ErrorAction SilentlyContinue
Remove-Item $nssmTmp -Recurse -Force -ErrorAction SilentlyContinue
Write-Ok "NSSM downloaded."
} catch {
Write-Err "Failed to download NSSM: $_"
Write-Info "You can download nssm.exe manually from https://nssm.cc/download"
Write-Info "Place nssm.exe next to this script and try again."
exit 1
}
}
# ── GitHub Release info ───────────────────────────────────────────────────────
function Get-LatestRelease {
Write-Step "Fetching latest release from GitHub..."
$apiUrl = "https://api.github.com/repos/$GitHubRepo/releases/latest"
$headers = @{ Accept = "application/vnd.github.v3+json"; "User-Agent" = "koreader-sync-installer" }
try {
$rel = Invoke-RestMethod -Uri $apiUrl -Headers $headers
} catch {
Write-Err "Could not reach GitHub API: $_"
Write-Info "Check your internet connection and that the repo '$GitHubRepo' is correct."
exit 1
}
$version = $rel.tag_name
$serverZip = $rel.assets |
Where-Object { $_.name -like "koreader-sync-server-*.zip" } |
Select-Object -First 1
if (-not $serverZip) {
Write-Err "No server zip found in release $version."
exit 1
}
Write-Ok "Latest release: $version"
return @{ Version = $version; ZipUrl = $serverZip.browser_download_url }
}
# ── Download and extract ──────────────────────────────────────────────────────
function Install-Bundle([string]$zipUrl, [string]$version) {
$tmpZip = Join-Path $env:TEMP "koreader-sync-server.zip"
$tmpDir = Join-Path $env:TEMP "koreader-sync-extract"
Write-Step "Downloading server bundle..."
Invoke-WebRequest -Uri $zipUrl -OutFile $tmpZip -UseBasicParsing
Write-Ok "Downloaded."
Write-Step "Extracting to $AppDir ..."
if (Test-Path $tmpDir) { Remove-Item $tmpDir -Recurse -Force }
Expand-Archive -Path $tmpZip -DestinationPath $tmpDir -Force
Remove-Item $tmpZip -Force -ErrorAction SilentlyContinue
if (-not (Test-Path $AppDir)) { New-Item -ItemType Directory -Path $AppDir | Out-Null }
# Replace app files (keep data directory untouched)
Get-ChildItem $tmpDir | Copy-Item -Destination $AppDir -Recurse -Force
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
# Write installed version marker
$version | Out-File -FilePath $VersionFile -Encoding utf8 -NoNewline
Write-Ok "Extracted version $version."
}
# ── Windows Service management ────────────────────────────────────────────────
function Get-ServiceStatus {
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($svc) { return $svc.Status } else { return $null }
}
function Install-Service {
$nodeExe = (Get-Command node -ErrorAction SilentlyContinue)?.Source
if (-not $nodeExe) { $nodeExe = "node" }
$entryPoint = Join-Path $AppDir "index.mjs"
Write-Step "Creating Windows service '$ServiceName'..."
& $NssmExe install $ServiceName $nodeExe $entryPoint
& $NssmExe set $ServiceName AppDirectory $AppDir
& $NssmExe set $ServiceName AppEnvironmentExtra `
"PORT=$Port" `
"KOREADER_DATA_DIR=$DataDir" `
"NODE_ENV=production"
& $NssmExe set $ServiceName DisplayName "KOReader Sync Server"
& $NssmExe set $ServiceName Description "KOReader reading progress sync server and dashboard"
& $NssmExe set $ServiceName Start SERVICE_AUTO_START
# Log files
$logDir = Join-Path $Base "logs"
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null }
& $NssmExe set $ServiceName AppStdout (Join-Path $logDir "koreader-sync.log")
& $NssmExe set $ServiceName AppStderr (Join-Path $logDir "koreader-sync-error.log")
& $NssmExe set $ServiceName AppRotateFiles 1
& $NssmExe set $ServiceName AppRotateSeconds 86400
Write-Ok "Service created."
}
function Start-KoService {
Write-Step "Starting service..."
& $NssmExe start $ServiceName | Out-Null
Start-Sleep -Seconds 2
$status = Get-ServiceStatus
if ($status -eq "Running") {
Write-Ok "Service running."
} else {
Write-Err "Service did not start (status: $status). Check logs at $(Join-Path $Base 'logs')"
}
}
function Stop-KoService {
$status = Get-ServiceStatus
if ($status -eq "Running") {
Write-Step "Stopping service for update..."
& $NssmExe stop $ServiceName | Out-Null
Start-Sleep -Seconds 3
Write-Ok "Service stopped."
}
}
# ── Uninstall ─────────────────────────────────────────────────────────────────
function Remove-Service {
Write-Header "Uninstalling KOReader Sync"
$status = Get-ServiceStatus
if ($status) {
Write-Step "Stopping and removing service '$ServiceName'..."
if ($status -eq "Running") {
& $NssmExe stop $ServiceName | Out-Null
Start-Sleep -Seconds 2
}
& $NssmExe remove $ServiceName confirm | Out-Null
Write-Ok "Service removed."
} else {
Write-Info "Service '$ServiceName' was not found — nothing to remove."
}
Write-Info "Data at '$DataDir' and app files at '$AppDir' were left intact."
Write-Info "Delete those directories manually if you want a full clean uninstall."
exit 0
}
# ── Status summary ────────────────────────────────────────────────────────────
function Show-Status([string]$version) {
Write-Header "KOReader Sync is ready"
Write-Host " Service : $ServiceName" -ForegroundColor White
Write-Host " Version : $version" -ForegroundColor White
Write-Host " Port : $Port" -ForegroundColor White
Write-Host " Data : $DataDir" -ForegroundColor White
Write-Host " Logs : $(Join-Path $Base 'logs')" -ForegroundColor White
Write-Host ""
Write-Host " Dashboard → http://localhost:$Port/" -ForegroundColor Cyan
Write-Host " Sync API → http://localhost:$Port/api/koreader" -ForegroundColor Cyan
Write-Host ""
Write-Host " KOReader: Tools → Progress sync → Custom server" -ForegroundColor Gray
Write-Host " URL: http://<this-server-ip>:$Port/api/koreader" -ForegroundColor Gray
Write-Host ""
}
# ─────────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────────
Write-Header "KOReader Sync Installer"
Assert-Admin
if ($Uninstall) {
Remove-Service
}
Assert-NodeJs
Get-Nssm
$release = Get-LatestRelease
$latestVersion = $release.Version
# Check if already installed
$installedVersion = if (Test-Path $VersionFile) {
(Get-Content $VersionFile -Raw).Trim()
} else { "" }
if ($installedVersion -eq "") {
# ── FRESH INSTALL ────────────────────────────────────────────────────────
Write-Header "Fresh Install"
Write-Info "Installing $latestVersion to $Base"
New-Item -ItemType Directory -Path $DataDir -Force | Out-Null
Install-Bundle -zipUrl $release.ZipUrl -version $latestVersion
$svcStatus = Get-ServiceStatus
if ($svcStatus) {
Write-Info "Service already exists (from a previous partial install). Removing and reinstalling."
& $NssmExe stop $ServiceName | Out-Null
& $NssmExe remove $ServiceName confirm | Out-Null
}
Install-Service
Start-KoService
Show-Status -version $latestVersion
} elseif ($installedVersion -eq $latestVersion) {
# ── ALREADY UP TO DATE ───────────────────────────────────────────────────
Write-Header "Already up to date"
Write-Ok "Version $installedVersion is the latest release."
$status = Get-ServiceStatus
if ($status -ne "Running") {
Write-Step "Service is not running. Starting it..."
Start-KoService
}
Show-Status -version $installedVersion
} else {
# ── UPDATE ───────────────────────────────────────────────────────────────
Write-Header "Updating $installedVersion → $latestVersion"
Stop-KoService
Install-Bundle -zipUrl $release.ZipUrl -version $latestVersion
Start-KoService
Show-Status -version $latestVersion
}