backup-ui-server.ps1 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. #Requires -Version 5.1
  2. <#
  3. .SYNOPSIS
  4. 独立备份页面:点选磁盘/文件夹,不手输路径。只监听 127.0.0.1。
  5. #>
  6. $ErrorActionPreference = "Stop"
  7. $RootDir = Split-Path -Parent $MyInvocation.MyCommand.Path
  8. $HtmlFile = Join-Path $RootDir "backup-ui.html"
  9. $BackupScript = Join-Path $RootDir "backup-db.ps1"
  10. $Port = 18765
  11. $Url = $null
  12. $listener = $null
  13. . (Join-Path $RootDir "db-config.ps1")
  14. if (-not (Test-Path $HtmlFile)) { throw "找不到 backup-ui.html" }
  15. if (-not (Test-Path $BackupScript)) { throw "找不到 backup-db.ps1" }
  16. Add-Type -AssemblyName System.Windows.Forms
  17. if (-not ("MesFolderPicker" -as [type])) {
  18. Add-Type -ReferencedAssemblies System.Windows.Forms,System.Drawing -TypeDefinition @"
  19. using System;
  20. using System.Runtime.InteropServices;
  21. using System.Threading;
  22. using System.Windows.Forms;
  23. public static class MesFolderPicker {
  24. [DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr hWnd);
  25. [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
  26. [DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
  27. [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr processId);
  28. [DllImport("kernel32.dll")] static extern uint GetCurrentThreadId();
  29. [DllImport("user32.dll")] static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
  30. [DllImport("user32.dll")] static extern bool BringWindowToTop(IntPtr hWnd);
  31. public static string Pick(string description) {
  32. string selected = null;
  33. Thread t = new Thread(delegate() {
  34. Form form = new Form();
  35. form.Text = "MES Backup - Select Folder";
  36. form.TopMost = true;
  37. form.ShowInTaskbar = true;
  38. form.StartPosition = FormStartPosition.CenterScreen;
  39. form.Width = 420;
  40. form.Height = 90;
  41. form.FormBorderStyle = FormBorderStyle.FixedToolWindow;
  42. form.MinimizeBox = false;
  43. form.MaximizeBox = false;
  44. form.Show();
  45. form.WindowState = FormWindowState.Normal;
  46. form.Activate();
  47. ForceForeground(form.Handle);
  48. Application.DoEvents();
  49. FolderBrowserDialog dlg = new FolderBrowserDialog();
  50. dlg.Description = description;
  51. dlg.ShowNewFolderButton = true;
  52. dlg.RootFolder = Environment.SpecialFolder.MyComputer;
  53. DialogResult r = dlg.ShowDialog(form);
  54. if (r == DialogResult.OK) {
  55. selected = dlg.SelectedPath;
  56. }
  57. form.Close();
  58. form.Dispose();
  59. });
  60. t.SetApartmentState(ApartmentState.STA);
  61. t.Start();
  62. t.Join();
  63. return selected;
  64. }
  65. static void ForceForeground(IntPtr hwnd) {
  66. IntPtr fg = GetForegroundWindow();
  67. uint fgThread = GetWindowThreadProcessId(fg, IntPtr.Zero);
  68. uint thisThread = GetCurrentThreadId();
  69. if (fgThread != thisThread) {
  70. AttachThreadInput(thisThread, fgThread, true);
  71. }
  72. ShowWindow(hwnd, 9);
  73. BringWindowToTop(hwnd);
  74. SetForegroundWindow(hwnd);
  75. if (fgThread != thisThread) {
  76. AttachThreadInput(thisThread, fgThread, false);
  77. }
  78. }
  79. }
  80. "@
  81. }
  82. $State = [hashtable]::Synchronized(@{
  83. Running = $false
  84. Log = New-Object System.Collections.ArrayList
  85. Result = ""
  86. Error = ""
  87. Path = ""
  88. StartedAt = ""
  89. EndedAt = ""
  90. Proc = $null
  91. OutFile = ""
  92. DbBytes = 0
  93. })
  94. function Add-UiLog {
  95. param([string]$Text)
  96. if ([string]::IsNullOrWhiteSpace($Text)) { return }
  97. [void]$State.Log.Add($Text)
  98. if ($State.Log.Count -gt 800) {
  99. $State.Log.RemoveRange(0, 200)
  100. }
  101. }
  102. function Get-SafePath {
  103. param([string]$Path)
  104. if ([string]::IsNullOrWhiteSpace($Path)) { throw "请先选择保存位置" }
  105. $p = $Path.Trim().Trim('"')
  106. if ($p -notmatch '^[A-Za-z]:\\') { throw "只允许本机磁盘路径" }
  107. $full = [System.IO.Path]::GetFullPath($p)
  108. if ($full -match '^[A-Za-z]:\\?$') {
  109. return ($full.Substring(0, 1).ToUpper() + ":\")
  110. }
  111. return $full.TrimEnd("\")
  112. }
  113. $script:MysqlToolCache = @{}
  114. function Find-MysqlTool {
  115. param([string]$Name)
  116. if ($script:MysqlToolCache.ContainsKey($Name)) { return $script:MysqlToolCache[$Name] }
  117. $cmd = Get-Command $Name -ErrorAction SilentlyContinue
  118. if ($cmd -and $cmd.Source) {
  119. $script:MysqlToolCache[$Name] = $cmd.Source
  120. return $cmd.Source
  121. }
  122. $candidates = New-Object System.Collections.Generic.List[string]
  123. foreach ($root in @(
  124. "C:\Program Files\MySQL",
  125. "C:\Program Files (x86)\MySQL",
  126. "C:\mysql",
  127. "D:\MySQL",
  128. "E:\MySQL",
  129. "C:\Program Files\MariaDB"
  130. )) {
  131. if (-not (Test-Path $root)) { continue }
  132. Get-ChildItem -Path $root -Directory -ErrorAction SilentlyContinue | ForEach-Object {
  133. $direct = Join-Path $_.FullName ("bin\{0}.exe" -f $Name)
  134. $candidates.Add($direct)
  135. Get-ChildItem -Path $_.FullName -Directory -ErrorAction SilentlyContinue | ForEach-Object {
  136. $candidates.Add((Join-Path $_.FullName ("bin\{0}.exe" -f $Name)))
  137. }
  138. }
  139. }
  140. foreach ($p in $candidates) {
  141. if (Test-Path -LiteralPath $p) {
  142. $script:MysqlToolCache[$Name] = $p
  143. return $p
  144. }
  145. }
  146. return $null
  147. }
  148. function Get-DbSizeBytes {
  149. try {
  150. $cfg = Read-MesDbConfig
  151. $mysql = Find-MysqlTool "mysql"
  152. if (-not $mysql) { return 0 }
  153. $cnf = Join-Path $env:TEMP ("mes_ui_size_{0}.cnf" -f $PID)
  154. Write-MesMysqlCnf -Config $cfg -CnfPath $cnf
  155. $sql = "SELECT IFNULL(SUM(data_length+index_length),0) FROM information_schema.tables WHERE table_schema='$($cfg.database)'"
  156. $out = & $mysql "--defaults-extra-file=$cnf" "--connect-timeout=3" -N -s -e $sql 2>$null
  157. Remove-Item $cnf -Force -ErrorAction SilentlyContinue
  158. $n = 0
  159. [void][int64]::TryParse(($out | Out-String).Trim(), [ref]$n)
  160. return $n
  161. } catch {
  162. return 0
  163. }
  164. }
  165. function Test-MesMysqlConn {
  166. param($Config)
  167. $cfg = ConvertTo-MesDbConfig -InputObject $Config
  168. $mysql = Find-MysqlTool "mysql"
  169. if (-not $mysql) { throw "找不到 mysql.exe,请安装 MySQL 客户端" }
  170. $cnf = Join-Path $env:TEMP ("mes_ui_test_{0}.cnf" -f $PID)
  171. Write-MesMysqlCnf -Config $cfg -CnfPath $cnf
  172. try {
  173. $out = & $mysql "--defaults-extra-file=$cnf" "--connect-timeout=5" -N -s -e "SELECT 1" 2>&1
  174. if ($LASTEXITCODE -ne 0) {
  175. throw ("连接失败: {0}" -f ($out | Out-String).Trim())
  176. }
  177. } finally {
  178. Remove-Item $cnf -Force -ErrorAction SilentlyContinue
  179. }
  180. return $cfg
  181. }
  182. function Get-DriveList {
  183. $list = New-Object System.Collections.Generic.List[object]
  184. foreach ($root in [System.IO.Directory]::GetLogicalDrives()) {
  185. if ($root -match '^[AB]:\\$') { continue }
  186. $letter = $root.Substring(0, 2).ToUpper()
  187. $list.Add([ordered]@{
  188. letter = $letter
  189. path = $root
  190. name = $letter
  191. type = "磁盘"
  192. removable = $false
  193. ready = $true
  194. sizeGB = 0
  195. freeGB = 0
  196. freeBytes = 0
  197. })
  198. }
  199. return $list
  200. }
  201. function Get-FolderList {
  202. param([string]$Path)
  203. $full = Get-SafePath $Path
  204. if (-not (Test-Path -LiteralPath $full)) { throw "路径不存在,请确认已插入磁盘" }
  205. $parent = [System.IO.Path]::GetDirectoryName($full)
  206. $folders = New-Object System.Collections.Generic.List[object]
  207. Get-ChildItem -LiteralPath $full -Directory -Force -ErrorAction SilentlyContinue |
  208. Select-Object -First 80 |
  209. ForEach-Object {
  210. $folders.Add([ordered]@{ name = $_.Name; path = $_.FullName })
  211. }
  212. $pathOut = $full
  213. if (-not $pathOut.EndsWith("\")) { $pathOut += "\" }
  214. return [ordered]@{
  215. path = $pathOut
  216. parent = $parent
  217. folders = $folders
  218. }
  219. }
  220. function Get-FreeBytes {
  221. param([string]$Path)
  222. try {
  223. $full = Get-SafePath $Path
  224. $root = [System.IO.Path]::GetPathRoot($full)
  225. $di = New-Object System.IO.DriveInfo ($root)
  226. if ($di.IsReady) { return [int64]$di.AvailableFreeSpace }
  227. } catch { }
  228. return 0
  229. }
  230. function Show-FolderPicker {
  231. return [MesFolderPicker]::Pick("选择备份文件保存位置(可选手动插入的 U 盘或移动硬盘)")
  232. }
  233. function Get-WrittenBackup {
  234. $info = [ordered]@{ bytes = 0; name = ""; doneBytes = 0 }
  235. $dir = $State.Path
  236. if (-not $dir -or -not (Test-Path -LiteralPath $dir)) { return $info }
  237. $files = @(Get-ChildItem -LiteralPath $dir -File -ErrorAction SilentlyContinue |
  238. Where-Object { $_.Name -like "mes_cloud_*.sql.gz*" } |
  239. Sort-Object LastWriteTime -Descending)
  240. if ($files.Count -gt 0) {
  241. $final = @($files | Where-Object { $_.Name -notlike "*.tmp" }) | Select-Object -First 1
  242. $pick = if ($final) { $final } else { $files[0] }
  243. $info.name = $pick.Name
  244. $info.bytes = [int64]$pick.Length
  245. if ($pick.Name -notlike "*.tmp") {
  246. $info.doneBytes = [int64]$pick.Length
  247. }
  248. }
  249. return $info
  250. }
  251. function Read-JsonBody {
  252. param($Request)
  253. $reader = New-Object System.IO.StreamReader($Request.InputStream, [System.Text.Encoding]::UTF8)
  254. try {
  255. $raw = $reader.ReadToEnd()
  256. } finally {
  257. $reader.Dispose()
  258. }
  259. if ([string]::IsNullOrWhiteSpace($raw)) { return $null }
  260. return $raw | ConvertFrom-Json
  261. }
  262. function Send-Bytes {
  263. param($Response, [byte[]]$Bytes, [string]$ContentType, [int]$Status = 200)
  264. $Response.StatusCode = $Status
  265. $Response.ContentType = $ContentType
  266. $Response.Headers["Cache-Control"] = "no-store"
  267. $Response.ContentLength64 = $Bytes.Length
  268. $Response.OutputStream.Write($Bytes, 0, $Bytes.Length)
  269. $Response.OutputStream.Close()
  270. }
  271. function Send-Json {
  272. param($Response, $Object, [int]$Status = 200)
  273. $json = $Object | ConvertTo-Json -Depth 8 -Compress
  274. $bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
  275. Send-Bytes -Response $Response -Bytes $bytes -ContentType "application/json; charset=utf-8" -Status $Status
  276. }
  277. function Send-Html {
  278. param($Response)
  279. $bytes = [System.IO.File]::ReadAllBytes($HtmlFile)
  280. Send-Bytes -Response $Response -Bytes $bytes -ContentType "text/html; charset=utf-8"
  281. }
  282. function Start-BackupJob {
  283. param([string]$TargetDir, [int]$KeepDays)
  284. if ($State.Running) { throw "已有备份正在进行" }
  285. $dir = Get-SafePath $TargetDir
  286. $free = Get-FreeBytes -Path $dir
  287. $dbBytes = Get-DbSizeBytes
  288. $State.DbBytes = $dbBytes
  289. # gzip 后通常小于库文件,但 SQL 文本可能接近库大小;预留 1.2 倍库容量
  290. if ($dbBytes -gt 0 -and $free -gt 0 -and $free -lt [int64]($dbBytes * 1.2)) {
  291. $needGB = [math]::Round($dbBytes * 1.2 / 1GB, 2)
  292. $freeGB = [math]::Round($free / 1GB, 2)
  293. throw ("目标盘剩余空间不足:约需 {0} GB,当前剩余 {1} GB。请换更大的 U 盘/硬盘。" -f $needGB, $freeGB)
  294. }
  295. if (-not (Test-Path -LiteralPath $dir)) {
  296. New-Item -ItemType Directory -Path $dir -Force | Out-Null
  297. }
  298. $outFile = Join-Path $env:TEMP ("mes_backup_ui_{0}.log" -f (Get-Date -Format "yyyyMMddHHmmss"))
  299. $errFile = $outFile + ".err"
  300. if (Test-Path $outFile) { Remove-Item $outFile -Force }
  301. if (Test-Path $errFile) { Remove-Item $errFile -Force }
  302. $arg = '-NoProfile -ExecutionPolicy Bypass -File "{0}" -BackupDir "{1}" -KeepDaysParam {2}' -f $BackupScript, $dir, $KeepDays
  303. $proc = Start-Process -FilePath "powershell.exe" -ArgumentList $arg -PassThru -WindowStyle Hidden `
  304. -RedirectStandardOutput $outFile -RedirectStandardError $errFile
  305. $State.Running = $true
  306. $State.Result = ""
  307. $State.Error = ""
  308. $State.Path = $dir
  309. $State.StartedAt = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
  310. $State.EndedAt = ""
  311. $State.Proc = $proc
  312. $State.OutFile = $outFile
  313. $State.Log.Clear()
  314. Add-UiLog ("开始备份到: {0}" -f $dir)
  315. if ($dbBytes -gt 0) {
  316. Add-UiLog ("库数据约 {0} GB,采用边导出边压缩,请耐心等待,勿拔盘" -f ([math]::Round($dbBytes / 1GB, 2)))
  317. }
  318. }
  319. function Read-LogFiles {
  320. $lines = New-Object System.Collections.ArrayList
  321. foreach ($f in @($State.OutFile, ($State.OutFile + ".err"))) {
  322. if (-not $f -or -not (Test-Path $f)) { continue }
  323. $text = $null
  324. foreach ($encName in @("UTF8", "Default")) {
  325. try {
  326. $enc = [System.Text.Encoding]::$encName
  327. $text = [System.IO.File]::ReadAllText($f, $enc)
  328. if ($text -and $text.IndexOf([char]0xFFFD) -lt 0) { break }
  329. } catch { }
  330. }
  331. if ($text) {
  332. foreach ($line in ($text -split "`r?`n")) {
  333. if ($line.Trim() -ne "") { [void]$lines.Add($line) }
  334. }
  335. }
  336. }
  337. return $lines
  338. }
  339. function Test-BackupFileOk {
  340. $dir = $State.Path
  341. if (-not $dir -or -not (Test-Path -LiteralPath $dir)) { return $false }
  342. $started = $null
  343. try {
  344. if ($State.StartedAt) { $started = [datetime]::Parse($State.StartedAt) }
  345. } catch { }
  346. $gz = Get-ChildItem -LiteralPath $dir -File -Filter "mes_cloud_*.sql.gz" -ErrorAction SilentlyContinue |
  347. Sort-Object LastWriteTime -Descending |
  348. Select-Object -First 1
  349. if ($null -eq $gz -or $gz.Length -le 100) { return $false }
  350. if ($started -and $gz.LastWriteTime -lt $started.AddMinutes(-2)) { return $false }
  351. return $true
  352. }
  353. function Update-BackupState {
  354. if (-not $State.Running) { return }
  355. $proc = $State.Proc
  356. $lines = Read-LogFiles
  357. if ($lines.Count -gt 0) {
  358. $State.Log.Clear()
  359. foreach ($line in $lines) { [void]$State.Log.Add($line) }
  360. }
  361. if ($null -eq $proc) { return }
  362. try { $proc.Refresh() } catch { }
  363. try { $exited = $proc.HasExited } catch { $exited = $true }
  364. if (-not $exited) { return }
  365. try { $proc.WaitForExit(1000) } catch { }
  366. $code = $null
  367. try { $code = $proc.ExitCode } catch { $code = $null }
  368. $State.Running = $false
  369. $State.EndedAt = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
  370. $State.Proc = $null
  371. $fileOk = Test-BackupFileOk
  372. # Start-Process 有时读不到 ExitCode(界面显示「退出码」后面空白);有新生成的 .gz 即视为成功
  373. $success = ($code -eq 0) -or $fileOk
  374. if ($success) {
  375. $State.Result = "ok"
  376. $State.Error = ""
  377. Add-UiLog "备份完成"
  378. } else {
  379. $State.Result = "error"
  380. $codeText = if ($null -eq $code) { "未知" } else { [string]$code }
  381. $State.Error = "备份失败,退出码 $codeText"
  382. Add-UiLog $State.Error
  383. }
  384. }
  385. $listener = $null
  386. $started = $false
  387. $lastError = $null
  388. foreach ($tryPort in 18765..18785) {
  389. $tryUrl = "http://127.0.0.1:{0}/" -f $tryPort
  390. $tryListener = New-Object System.Net.HttpListener
  391. try {
  392. $tryListener.Prefixes.Add($tryUrl)
  393. $tryListener.Start()
  394. $listener = $tryListener
  395. $Port = $tryPort
  396. $Url = $tryUrl
  397. $started = $true
  398. break
  399. } catch {
  400. $lastError = $_.Exception.Message
  401. try { $tryListener.Close() } catch { }
  402. }
  403. }
  404. if (-not $started) {
  405. throw ("无法启动本机页面,18765-18785 端口都被占用。请先关掉之前打开的备份窗口后再试。详情: {0}" -f $lastError)
  406. }
  407. Write-Host ""
  408. Write-Host ("数据库备份页面已启动(仅本机): {0}" -f $Url)
  409. if ($Port -ne 18765) {
  410. Write-Host ("原端口 18765 已被占用,已自动改用 {0}。请先关掉旧的备份窗口,避免开两个。" -f $Port)
  411. }
  412. Write-Host "请在网页点「浏览文件夹」选择 U 盘/硬盘目录,不要手输路径。"
  413. Write-Host "关闭本窗口即停止页面。"
  414. Write-Host ""
  415. Start-Process $Url
  416. try {
  417. while ($listener.IsListening) {
  418. $ctx = $null
  419. try {
  420. $ctx = $listener.GetContext()
  421. $req = $ctx.Request
  422. $res = $ctx.Response
  423. $path = $req.Url.AbsolutePath.TrimEnd("/")
  424. if ([string]::IsNullOrEmpty($path)) { $path = "/" }
  425. if ($req.HttpMethod -eq "OPTIONS") {
  426. $res.StatusCode = 204
  427. $res.AddHeader("Access-Control-Allow-Origin", "*")
  428. $res.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
  429. $res.AddHeader("Access-Control-Allow-Headers", "Content-Type")
  430. $res.ContentLength64 = 0
  431. $res.Close()
  432. continue
  433. }
  434. if ($req.HttpMethod -eq "GET" -and ($path -eq "/" -or $path -eq "/index.html")) {
  435. Send-Html -Response $res
  436. continue
  437. }
  438. if ($req.HttpMethod -eq "GET" -and $path -eq "/api/drives") {
  439. Send-Json -Response $res -Object ([ordered]@{
  440. ok = $true
  441. drives = @(Get-DriveList)
  442. })
  443. continue
  444. }
  445. if ($req.HttpMethod -eq "GET" -and $path -eq "/api/dbsize") {
  446. Send-Json -Response $res -Object ([ordered]@{
  447. ok = $true
  448. dbBytes = [int64](Get-DbSizeBytes)
  449. })
  450. continue
  451. }
  452. if ($req.HttpMethod -eq "GET" -and $path -eq "/api/folders") {
  453. $folderPath = ""
  454. $query = $req.Url.Query.TrimStart("?")
  455. if ($query) {
  456. foreach ($pair in $query.Split([char]38)) {
  457. $kv = $pair.Split("=", 2)
  458. if ($kv.Count -ge 2 -and [uri]::UnescapeDataString($kv[0]) -eq "path") {
  459. $folderPath = [uri]::UnescapeDataString(($kv[1] -replace "\+", " "))
  460. }
  461. }
  462. }
  463. Send-Json -Response $res -Object ([ordered]@{ ok = $true; data = (Get-FolderList -Path $folderPath) })
  464. continue
  465. }
  466. if ($req.HttpMethod -eq "POST" -and $path -eq "/api/browse") {
  467. $picked = Show-FolderPicker
  468. if ([string]::IsNullOrWhiteSpace($picked)) {
  469. Send-Json -Response $res -Object ([ordered]@{ ok = $true; cancelled = $true })
  470. } else {
  471. Send-Json -Response $res -Object ([ordered]@{
  472. ok = $true
  473. cancelled = $false
  474. path = $picked
  475. freeBytes = [int64](Get-FreeBytes -Path $picked)
  476. })
  477. }
  478. continue
  479. }
  480. if ($req.HttpMethod -eq "GET" -and $path -eq "/api/status") {
  481. Update-BackupState
  482. $written = Get-WrittenBackup
  483. Send-Json -Response $res -Object ([ordered]@{
  484. ok = $true
  485. running = [bool]$State.Running
  486. result = $State.Result
  487. error = $State.Error
  488. path = $State.Path
  489. startedAt = $State.StartedAt
  490. endedAt = $State.EndedAt
  491. log = @($State.Log)
  492. dbBytes = [int64]$State.DbBytes
  493. writtenBytes = [int64]$written.bytes
  494. fileName = $written.name
  495. })
  496. continue
  497. }
  498. if ($req.HttpMethod -eq "GET" -and $path -eq "/api/config") {
  499. try {
  500. $cfg = Read-MesDbConfig
  501. } catch {
  502. $cfg = Get-DefaultMesDbConfig
  503. }
  504. Send-Json -Response $res -Object ([ordered]@{
  505. ok = $true
  506. engine = "mysql"
  507. host = [string]$cfg.host
  508. port = [int]$cfg.port
  509. database = [string]$cfg.database
  510. user = [string]$cfg.user
  511. password = [string]$cfg.password
  512. })
  513. continue
  514. }
  515. if ($req.HttpMethod -eq "POST" -and $path -eq "/api/config") {
  516. $body = Read-JsonBody -Request $req
  517. $cfg = Save-MesDbConfig -Config $body
  518. Send-Json -Response $res -Object ([ordered]@{
  519. ok = $true
  520. message = "已保存 MySQL 配置"
  521. host = [string]$cfg.host
  522. port = [int]$cfg.port
  523. database = [string]$cfg.database
  524. user = [string]$cfg.user
  525. })
  526. continue
  527. }
  528. if (($req.HttpMethod -eq "GET" -or $req.HttpMethod -eq "POST") -and $path -eq "/api/test") {
  529. $body = $null
  530. if ($req.HttpMethod -eq "POST") {
  531. $body = Read-JsonBody -Request $req
  532. }
  533. if ($null -eq $body) {
  534. $q = $req.Url.Query.TrimStart("?")
  535. $map = @{}
  536. if ($q) {
  537. foreach ($pair in $q.Split([char]38)) {
  538. $kv = $pair.Split("=", 2)
  539. if ($kv.Count -ge 2) {
  540. $map[[uri]::UnescapeDataString($kv[0])] = [uri]::UnescapeDataString(($kv[1] -replace "\+", " "))
  541. }
  542. }
  543. }
  544. $body = [pscustomobject]@{
  545. engine = "mysql"
  546. host = $map["host"]
  547. port = $map["port"]
  548. database = $map["database"]
  549. user = $map["user"]
  550. password = $map["password"]
  551. }
  552. }
  553. $cfg = Test-MesMysqlConn -Config $body
  554. Send-Json -Response $res -Object ([ordered]@{
  555. ok = $true
  556. message = ("MySQL 连接成功: {0}:{1} / {2}" -f $cfg.host, $cfg.port, $cfg.database)
  557. })
  558. continue
  559. }
  560. if ($req.HttpMethod -eq "POST" -and $path -eq "/api/backup") {
  561. $body = Read-JsonBody -Request $req
  562. if ($body -and $body.host) {
  563. Save-MesDbConfig -Config $body | Out-Null
  564. }
  565. $keep = 0
  566. if ($body -and $null -ne $body.keepDays) { $keep = [int]$body.keepDays }
  567. Start-BackupJob -TargetDir $body.path -KeepDays $keep
  568. Send-Json -Response $res -Object ([ordered]@{ ok = $true; message = "已开始备份" })
  569. continue
  570. }
  571. Send-Json -Response $res -Object ([ordered]@{ ok = $false; message = "未找到接口" }) -Status 404
  572. } catch {
  573. if ($null -ne $ctx) {
  574. try {
  575. Send-Json -Response $ctx.Response -Object ([ordered]@{ ok = $false; message = $_.Exception.Message }) -Status 500
  576. } catch { }
  577. }
  578. }
  579. }
  580. } finally {
  581. if ($listener) {
  582. try { $listener.Stop() } catch { }
  583. try { $listener.Close() } catch { }
  584. }
  585. }