|
| 1 | +param( |
| 2 | + [Parameter(Mandatory = $false)] |
| 3 | + [string]$CredFile = "..\\mima.txt", |
| 4 | + |
| 5 | + [Parameter(Mandatory = $false)] |
| 6 | + [string]$HostName, |
| 7 | + |
| 8 | + [Parameter(Mandatory = $false)] |
| 9 | + [int]$Port = 22, |
| 10 | + |
| 11 | + [Parameter(Mandatory = $false)] |
| 12 | + [string]$UserName, |
| 13 | + |
| 14 | + [Parameter(Mandatory = $false)] |
| 15 | + [string]$Password, |
| 16 | + |
| 17 | + [Parameter(Mandatory = $false)] |
| 18 | + [string]$ServiceName = "linkbridge", |
| 19 | + |
| 20 | + [Parameter(Mandatory = $false)] |
| 21 | + [string]$RemoteTmpPath = "/tmp/linkbridge-backend", |
| 22 | + |
| 23 | + [Parameter(Mandatory = $false)] |
| 24 | + [string]$RemoteInstallPath = "/opt/linkbridge-backend" |
| 25 | +) |
| 26 | + |
| 27 | +Set-StrictMode -Version Latest |
| 28 | +$ErrorActionPreference = "Stop" |
| 29 | + |
| 30 | +function Assert-CommandExists { |
| 31 | + param([Parameter(Mandatory = $true)][string]$Name) |
| 32 | + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { |
| 33 | + throw "Missing command '$Name'." |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +function Ensure-WinSCP { |
| 38 | + $toolsDir = Join-Path $PSScriptRoot ".tools\\winscp" |
| 39 | + $exeCom = Join-Path $toolsDir "WinSCP.com" |
| 40 | + $exeGui = Join-Path $toolsDir "WinSCP.exe" |
| 41 | + if ((Test-Path $exeCom) -and (Test-Path $exeGui)) { |
| 42 | + return $exeCom |
| 43 | + } |
| 44 | + |
| 45 | + New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null |
| 46 | + |
| 47 | + # Official "downloading..." page (contains a time-limited CDN link to the ZIP). |
| 48 | + # Note: We intentionally fetch the HTML page first, then resolve the real ZIP URL, |
| 49 | + # otherwise we may accidentally download HTML and treat it as a ZIP. |
| 50 | + $downloadPageUrl = "https://winscp.net/download/WinSCP-6.3.6-Portable.zip" |
| 51 | + Write-Host "=== Resolve WinSCP portable ZIP URL ===" |
| 52 | + $page = Invoke-WebRequest -Uri $downloadPageUrl -UseBasicParsing |
| 53 | + $html = $page.Content |
| 54 | + $zipUrl = $null |
| 55 | + $m = [regex]::Match($html, '(https?://)?cdn\.winscp\.net/files/WinSCP-[^"\s>]+-Portable\.zip\?secure=[^"\s>]+', 'IgnoreCase') |
| 56 | + if ($m.Success) { |
| 57 | + $zipUrl = $m.Value |
| 58 | + if (-not $zipUrl.StartsWith("http")) { |
| 59 | + $zipUrl = "https://" + $zipUrl |
| 60 | + } |
| 61 | + } |
| 62 | + if (-not $zipUrl) { |
| 63 | + $m2 = [regex]::Match($html, 'https?://sourceforge\.net/projects/winscp/files/WinSCP/[^"\s>]+/WinSCP-[^"\s>]+-Portable\.zip/download', 'IgnoreCase') |
| 64 | + if ($m2.Success) { |
| 65 | + $zipUrl = $m2.Value |
| 66 | + } |
| 67 | + } |
| 68 | + if (-not $zipUrl) { |
| 69 | + throw "Failed to resolve WinSCP portable ZIP URL from $downloadPageUrl" |
| 70 | + } |
| 71 | + |
| 72 | + # Download ZIP and unzip only WinSCP.com for scripting. |
| 73 | + $zipPath = Join-Path $toolsDir "winscp-portable.zip" |
| 74 | + |
| 75 | + Write-Host "=== Download WinSCP portable CLI ===" |
| 76 | + Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing |
| 77 | + |
| 78 | + Write-Host "=== Extract WinSCP.com + WinSCP.exe ===" |
| 79 | + $sig = [System.IO.File]::ReadAllBytes($zipPath)[0..1] |
| 80 | + if (-not ($sig[0] -eq 0x50 -and $sig[1] -eq 0x4B)) { |
| 81 | + $head = [System.Text.Encoding]::ASCII.GetString([System.IO.File]::ReadAllBytes($zipPath)[0..120]) |
| 82 | + $head = $head -replace "`r", "" -replace "`n", " " |
| 83 | + throw ("Downloaded file is not a ZIP (missing PK signature). Head: " + $head) |
| 84 | + } |
| 85 | + |
| 86 | + Add-Type -AssemblyName System.IO.Compression.FileSystem |
| 87 | + $zip = [System.IO.Compression.ZipFile]::OpenRead($zipPath) |
| 88 | + try { |
| 89 | + $foundCom = $false |
| 90 | + $foundExe = $false |
| 91 | + foreach ($entry in $zip.Entries) { |
| 92 | + if ($entry.Name -ieq "WinSCP.com") { |
| 93 | + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $exeCom, $true) |
| 94 | + $foundCom = $true |
| 95 | + continue |
| 96 | + } |
| 97 | + if ($entry.Name -ieq "WinSCP.exe") { |
| 98 | + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $exeGui, $true) |
| 99 | + $foundExe = $true |
| 100 | + continue |
| 101 | + } |
| 102 | + } |
| 103 | + if (-not $foundCom -or -not $foundExe) { |
| 104 | + throw "WinSCP portable ZIP did not contain required files (WinSCP.com/WinSCP.exe)." |
| 105 | + } |
| 106 | + } |
| 107 | + finally { |
| 108 | + $zip.Dispose() |
| 109 | + } |
| 110 | + |
| 111 | + Remove-Item -Force $zipPath -ErrorAction SilentlyContinue |
| 112 | + |
| 113 | + if (-not (Test-Path $exeCom) -or -not (Test-Path $exeGui)) { |
| 114 | + throw "WinSCP.com/WinSCP.exe not found after extraction." |
| 115 | + } |
| 116 | + return $exeCom |
| 117 | +} |
| 118 | + |
| 119 | +function Read-Creds { |
| 120 | + param([Parameter(Mandatory = $true)][string]$Path) |
| 121 | + $full = Resolve-Path $Path |
| 122 | + $lines = Get-Content $full | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" } |
| 123 | + if ($lines.Count -lt 3) { |
| 124 | + throw "Credential file must contain at least 3 non-empty lines: host, username, password." |
| 125 | + } |
| 126 | + return @{ |
| 127 | + Host = $lines[0] |
| 128 | + User = $lines[1] |
| 129 | + Pass = $lines[2] |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +Assert-CommandExists "go" |
| 134 | + |
| 135 | +if (-not $HostName -or -not $UserName -or -not $Password) { |
| 136 | + $c = Read-Creds -Path (Join-Path $PSScriptRoot $CredFile) |
| 137 | + if (-not $HostName) { $HostName = $c.Host } |
| 138 | + if (-not $UserName) { $UserName = $c.User } |
| 139 | + if (-not $Password) { $Password = $c.Pass } |
| 140 | +} |
| 141 | + |
| 142 | +if (-not $HostName -or -not $UserName -or -not $Password) { |
| 143 | + throw "Missing HostName/UserName/Password" |
| 144 | +} |
| 145 | + |
| 146 | +$winscp = Ensure-WinSCP |
| 147 | + |
| 148 | +Push-Location $PSScriptRoot |
| 149 | +try { |
| 150 | + $outFile = Join-Path $PSScriptRoot "linkbridge-backend" |
| 151 | + |
| 152 | + Write-Host "=== Build linux amd64 binary ===" |
| 153 | + $oldEnv = @{ |
| 154 | + CGO_ENABLED = $env:CGO_ENABLED |
| 155 | + GOOS = $env:GOOS |
| 156 | + GOARCH = $env:GOARCH |
| 157 | + } |
| 158 | + try { |
| 159 | + $env:CGO_ENABLED = "0" |
| 160 | + $env:GOOS = "linux" |
| 161 | + $env:GOARCH = "amd64" |
| 162 | + if (Test-Path $outFile) { Remove-Item -Force $outFile } |
| 163 | + & go build -o $outFile ./cmd/api |
| 164 | + if ($LASTEXITCODE -ne 0) { throw "go build failed" } |
| 165 | + } |
| 166 | + finally { |
| 167 | + $env:CGO_ENABLED = $oldEnv.CGO_ENABLED |
| 168 | + $env:GOOS = $oldEnv.GOOS |
| 169 | + $env:GOARCH = $oldEnv.GOARCH |
| 170 | + } |
| 171 | + |
| 172 | + Write-Host "=== Upload + deploy via WinSCP (password auth) ===" |
| 173 | + $tmp = Join-Path $env:TEMP ("winscp-deploy-" + [Guid]::NewGuid().ToString("N") + ".txt") |
| 174 | + $passFile = Join-Path $env:TEMP ("winscp-pass-" + [Guid]::NewGuid().ToString("N") + ".txt") |
| 175 | + |
| 176 | + # SECURITY NOTE: |
| 177 | + # - Password is stored only in a temporary file (deleted after), and passed to WinSCP using -passwordsfromfiles. |
| 178 | + # - The WinSCP script does not contain the password, so we can safely print WinSCP output on failure. |
| 179 | + $sessionUrl = "sftp://{0}@{1}:{2}/" -f $UserName, $HostName, $Port |
| 180 | + |
| 181 | + # WinSCP reads only first line; keep it simple. |
| 182 | + Set-Content -Path $passFile -Value $Password -Encoding UTF8 |
| 183 | + |
| 184 | + $script = @" |
| 185 | +option batch abort |
| 186 | +option confirm off |
| 187 | +open $sessionUrl -password="$passFile" -passwordsfromfiles -hostkey="*" |
| 188 | +put `"$outFile`" $RemoteTmpPath |
| 189 | +call systemctl stop $ServiceName || true |
| 190 | +call mv $RemoteTmpPath $RemoteInstallPath |
| 191 | +call chmod +x $RemoteInstallPath |
| 192 | +call systemctl start $ServiceName |
| 193 | +call sleep 2 |
| 194 | +call systemctl status $ServiceName --no-pager |
| 195 | +exit |
| 196 | +"@ |
| 197 | + Set-Content -Path $tmp -Value $script -Encoding ASCII |
| 198 | + |
| 199 | + try { |
| 200 | + $out = & $winscp "/script=$tmp" |
| 201 | + if ($LASTEXITCODE -ne 0) { |
| 202 | + if ($out) { Write-Host ($out -join "`n") } |
| 203 | + throw "WinSCP deploy failed (exit=$LASTEXITCODE)." |
| 204 | + } |
| 205 | + } |
| 206 | + finally { |
| 207 | + Remove-Item -Force $tmp -ErrorAction SilentlyContinue |
| 208 | + Remove-Item -Force $passFile -ErrorAction SilentlyContinue |
| 209 | + } |
| 210 | + |
| 211 | + Write-Host "=== Cleanup local binary ===" |
| 212 | + if (Test-Path $outFile) { Remove-Item -Force $outFile } |
| 213 | + |
| 214 | + Write-Host "=== Done ===" |
| 215 | +} |
| 216 | +finally { |
| 217 | + Pop-Location |
| 218 | +} |
0 commit comments