447 lines
16 KiB
PowerShell
447 lines
16 KiB
PowerShell
# Deploy Script - Windows PowerShell
|
||
# Upload source code directly to server, build with Docker on server
|
||
# No need to test and build locally
|
||
|
||
param(
|
||
[string]$Server = "45.249.208.118",
|
||
[string]$User = "root",
|
||
[string]$Password = "",
|
||
[string]$ProjectPath = "/root/youyidian-3d",
|
||
[switch]$SkipBackup = $false,
|
||
[switch]$SkipCheck = $false
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
# UTF-8 Encoding
|
||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
||
$PSDefaultParameterValues['*:Encoding'] = 'utf8'
|
||
}
|
||
|
||
# 函数:获取服务器密钥指纹(用于 plink/pscp 的 -hostkey 参数)
|
||
function Get-ServerHostKey {
|
||
param([string]$Server)
|
||
|
||
$hostKey = $null
|
||
try {
|
||
# 方法1: 先从 known_hosts 文件中读取(如果已经连接过服务器)
|
||
$knownHostsPath = Join-Path $env:USERPROFILE ".ssh\known_hosts"
|
||
if (Test-Path $knownHostsPath) {
|
||
$knownHostsContent = Get-Content $knownHostsPath -ErrorAction SilentlyContinue
|
||
if ($knownHostsContent) {
|
||
$matchingLines = $knownHostsContent | Where-Object {
|
||
$_ -notmatch '^\s*#' -and
|
||
$_ -match '\s+(ssh-\w+|ecdsa-sha2-\S+)\s+' -and
|
||
($_ -match [regex]::Escape($Server) -or $_ -match "^$Server,")
|
||
}
|
||
|
||
if ($matchingLines) {
|
||
$keyLine = $matchingLines | Where-Object { $_ -match 'ssh-ed25519' } | Select-Object -First 1
|
||
if (-not $keyLine) {
|
||
$keyLine = $matchingLines | Where-Object { $_ -match 'ecdsa-sha2' } | Select-Object -First 1
|
||
}
|
||
if (-not $keyLine) {
|
||
$keyLine = $matchingLines | Where-Object { $_ -match 'ssh-rsa' } | Select-Object -First 1
|
||
}
|
||
|
||
if ($keyLine -and $keyLine -match '\s+(ssh-\w+|ecdsa-sha2-\S+)\s+(\S+)') {
|
||
$keyType = $matches[1]
|
||
$keyData = $matches[2]
|
||
$tempKeyFile = [System.IO.Path]::GetTempFileName()
|
||
try {
|
||
"$Server $keyType $keyData" | Out-File -FilePath $tempKeyFile -Encoding ASCII -NoNewline
|
||
$fingerprintOutput = & ssh-keygen -lf $tempKeyFile -E sha256 2>$null
|
||
if ($fingerprintOutput) {
|
||
$fingerprint = ($fingerprintOutput -split '\s+')[1]
|
||
if ($fingerprint) {
|
||
# plink/pscp 的 -hostkey 格式:仅使用 SHA256:指纹(不需要算法名)
|
||
# fingerprint 已经包含 SHA256: 前缀
|
||
$hostKey = $fingerprint
|
||
return $hostKey
|
||
}
|
||
}
|
||
} finally {
|
||
Remove-Item -Path $tempKeyFile -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# 方法2: 使用 ssh-keyscan 获取服务器公钥
|
||
$keyscanOutput = ssh-keyscan -t ed25519,ecdsa,rsa $Server 2>$null
|
||
if ($keyscanOutput) {
|
||
$keyLine = $keyscanOutput | Where-Object { $_ -notmatch '^\s*#' -and $_ -match 'ssh-ed25519' } | Select-Object -First 1
|
||
if (-not $keyLine) {
|
||
$keyLine = $keyscanOutput | Where-Object { $_ -notmatch '^\s*#' -and $_ -match 'ecdsa-sha2' } | Select-Object -First 1
|
||
}
|
||
if (-not $keyLine) {
|
||
$keyLine = $keyscanOutput | Where-Object { $_ -notmatch '^\s*#' -and $_ -match 'ssh-rsa' } | Select-Object -First 1
|
||
}
|
||
if ($keyLine) {
|
||
$keyLine = $keyLine.ToString().Trim()
|
||
if ($keyLine -match '^\S+\s+(ssh-\w+)\s+(\S+)') {
|
||
$keyType = $matches[1]
|
||
$keyData = $matches[2]
|
||
$tempKeyFile = [System.IO.Path]::GetTempFileName()
|
||
try {
|
||
"$keyLine" | Out-File -FilePath $tempKeyFile -Encoding ASCII -NoNewline
|
||
$fingerprintOutput = & ssh-keygen -lf $tempKeyFile -E sha256 2>$null
|
||
if ($fingerprintOutput) {
|
||
$fingerprint = ($fingerprintOutput -split '\s+')[1]
|
||
if ($fingerprint) {
|
||
# plink/pscp 的 -hostkey 格式:仅使用 SHA256:指纹(不需要算法名)
|
||
# fingerprint 已经包含 SHA256: 前缀
|
||
$hostKey = $fingerprint
|
||
}
|
||
}
|
||
} finally {
|
||
Remove-Item -Path $tempKeyFile -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
# 如果获取密钥失败,返回 null
|
||
}
|
||
|
||
return $hostKey
|
||
}
|
||
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host "安全部署脚本 - 直接上传源代码" -ForegroundColor Green
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host ""
|
||
|
||
# 检查当前目录
|
||
if (-not (Test-Path "package.json")) {
|
||
Write-Host "错误: 请在项目根目录运行此脚本" -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
|
||
# 步骤 1: 部署前检查(已移除Git检查,不需要上传到git库)
|
||
if (-not $SkipCheck) {
|
||
Write-Host "[1/5] 执行部署前检查..." -ForegroundColor Yellow
|
||
|
||
# 只检查必要文件是否存在
|
||
if (-not (Test-Path "package.json")) {
|
||
Write-Host " ✗ 未找到 package.json 文件" -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
|
||
if (-not (Test-Path "prisma/schema.prisma")) {
|
||
Write-Host " ⚠ 未找到 prisma/schema.prisma 文件" -ForegroundColor Yellow
|
||
}
|
||
|
||
Write-Host " ✓ 部署前检查通过" -ForegroundColor Green
|
||
Write-Host ""
|
||
}
|
||
|
||
# 步骤 2: 打包项目文件(只包含功能性代码,排除非功能性文件)
|
||
Write-Host "[2/5] 打包项目文件..." -ForegroundColor Yellow
|
||
Write-Host " 注意: 只打包功能性代码,排除文档、脚本、临时文件等" -ForegroundColor Gray
|
||
Write-Host " 排除内容: docs/, scripts/, *.md, uploads/, backups/, .env.local 等" -ForegroundColor Gray
|
||
|
||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||
$tempFile = Join-Path $env:TEMP "project-deploy-$timestamp.tar.gz"
|
||
|
||
# 排除列表(与 .dockerignore 保持一致,但适配 tar 命令)
|
||
$excludes = @(
|
||
# 构建产物和依赖
|
||
"node_modules",
|
||
".next",
|
||
"out",
|
||
"build",
|
||
"dist",
|
||
".pnp",
|
||
".pnp.js",
|
||
|
||
# 测试文件
|
||
"coverage",
|
||
"*.test.ts",
|
||
"*.test.tsx",
|
||
"*.spec.ts",
|
||
"*.spec.tsx",
|
||
"test",
|
||
"tests",
|
||
"__tests__",
|
||
|
||
# 日志和调试
|
||
"*.log",
|
||
"logs",
|
||
"*.log.*",
|
||
"npm-debug.log*",
|
||
"yarn-debug.log*",
|
||
"yarn-error.log*",
|
||
|
||
# 环境变量
|
||
".env.local",
|
||
".env.development.local",
|
||
".env.test.local",
|
||
".env.production.local",
|
||
".env",
|
||
".env.example",
|
||
".env.template",
|
||
".env*.local",
|
||
|
||
# 版本控制
|
||
".git",
|
||
".gitignore",
|
||
".gitattributes",
|
||
".gitmodules",
|
||
|
||
# IDE 和编辑器
|
||
".vscode",
|
||
".idea",
|
||
".cursor",
|
||
"*.swp",
|
||
"*.swo",
|
||
"*.sublime-project",
|
||
"*.sublime-workspace",
|
||
".editorconfig",
|
||
".eslintcache",
|
||
|
||
# 文档文件
|
||
"docs",
|
||
"*.md",
|
||
"README.md",
|
||
"LICENSE",
|
||
"CHANGELOG.md",
|
||
"CONTRIBUTING.md",
|
||
# 注意:不排除 *.txt,因为 VERSION.txt 需要被包含
|
||
# 其他不需要的 .txt 文件会在文档排除中处理
|
||
|
||
# 脚本文件
|
||
"scripts",
|
||
"*.sh",
|
||
"*.ps1",
|
||
"*.bat",
|
||
"*.py",
|
||
"*.js",
|
||
"!next.config.js",
|
||
"!postcss.config.js",
|
||
"!tailwind.config.ts",
|
||
|
||
# 临时文件和缓存
|
||
"uploads",
|
||
"public/uploads",
|
||
"backups",
|
||
"*.tmp",
|
||
"*.temp",
|
||
".cache",
|
||
".tmp",
|
||
"tmp",
|
||
"temp",
|
||
"*.cache",
|
||
".DS_Store",
|
||
"Thumbs.db",
|
||
|
||
# 压缩包和发布包
|
||
"*.tar.gz",
|
||
"*.zip",
|
||
"*.rar",
|
||
"*.7z",
|
||
"*.tar",
|
||
"*.tgz",
|
||
"release-*",
|
||
"dist-*",
|
||
|
||
# 数据库备份文件
|
||
"*.sql",
|
||
"*.db",
|
||
"*.sqlite",
|
||
"*.sqlite3",
|
||
|
||
# 其他文件
|
||
"*.rpm",
|
||
"*.deb",
|
||
"*.pem",
|
||
"*.key",
|
||
"*.cert",
|
||
"*.crt",
|
||
"frp_*",
|
||
"docker-daemon-config.json"
|
||
)
|
||
|
||
# 构建 tar 排除参数
|
||
$excludeArgs = $excludes | ForEach-Object { "--exclude=$_" }
|
||
|
||
# 打包文件
|
||
Write-Host " 正在打包..." -ForegroundColor Gray
|
||
& tar -czf $tempFile $excludeArgs *
|
||
|
||
if (-not (Test-Path $tempFile)) {
|
||
Write-Host " ✗ 打包失败" -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
|
||
$fileSize = (Get-Item $tempFile).Length / 1MB
|
||
Write-Host " ✓ 打包完成: $([math]::Round($fileSize, 2)) MB" -ForegroundColor Green
|
||
Write-Host ""
|
||
|
||
# 步骤 3: 上传到服务器
|
||
Write-Host "[3/5] 上传文件到服务器..." -ForegroundColor Yellow
|
||
Write-Host " 服务器: $Server" -ForegroundColor Gray
|
||
Write-Host " 用户: $User" -ForegroundColor Gray
|
||
Write-Host " 项目路径: $ProjectPath" -ForegroundColor Gray
|
||
Write-Host ""
|
||
|
||
# 检查是否有 plink(PuTTY 工具)
|
||
$plinkPath = "C:\Program Files\PuTTY\plink.exe"
|
||
$pscpPath = "C:\Program Files\PuTTY\pscp.exe"
|
||
$usePlink = (Test-Path $plinkPath) -and (Test-Path $pscpPath) -and ($Password -ne "")
|
||
|
||
# 检查 SSH 连接
|
||
Write-Host " 检查 SSH 连接..." -ForegroundColor Gray
|
||
if ($usePlink) {
|
||
Write-Host " 使用 plink 进行连接(已提供密码)" -ForegroundColor Gray
|
||
# 获取服务器密钥
|
||
$hostKey = Get-ServerHostKey -Server $Server
|
||
if ($hostKey) {
|
||
& $plinkPath -ssh -pw $Password -batch -hostkey $hostKey "${User}@${Server}" "echo SSH连接成功" 2>&1 | Out-Null
|
||
} else {
|
||
& $plinkPath -ssh -pw $Password -batch "${User}@${Server}" "echo SSH连接成功" 2>&1 | Out-Null
|
||
}
|
||
} else {
|
||
Write-Host " 提示: 如果需要,请输入 SSH 密码" -ForegroundColor Yellow
|
||
ssh -o ConnectTimeout=10 "${User}@${Server}" "echo SSH连接成功" 2>&1 | Out-Null
|
||
}
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Host " ✗ SSH 连接失败,请检查连接配置" -ForegroundColor Red
|
||
if (-not $usePlink) {
|
||
Write-Host " 提示: 请确保已配置 SSH 密钥或密码正确" -ForegroundColor Yellow
|
||
}
|
||
exit 1
|
||
}
|
||
Write-Host " ✓ SSH 连接成功" -ForegroundColor Green
|
||
Write-Host ""
|
||
|
||
# 上传压缩包
|
||
Write-Host " 上传压缩包..." -ForegroundColor Gray
|
||
$remoteFileName = "project-deploy.tar.gz"
|
||
if ($usePlink) {
|
||
# 获取服务器密钥(如果之前没有获取)
|
||
if (-not $hostKey) {
|
||
$hostKey = Get-ServerHostKey -Server $Server
|
||
}
|
||
if ($hostKey) {
|
||
& $pscpPath -pw $Password -batch -hostkey $hostKey $tempFile "${User}@${Server}:${ProjectPath}/$remoteFileName" 2>&1 | Out-Null
|
||
} else {
|
||
& $pscpPath -pw $Password -batch $tempFile "${User}@${Server}:${ProjectPath}/$remoteFileName" 2>&1 | Out-Null
|
||
}
|
||
} else {
|
||
scp $tempFile "${User}@${Server}:${ProjectPath}/$remoteFileName" 2>&1 | Out-Null
|
||
}
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Host " ✗ 上传失败" -ForegroundColor Red
|
||
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
|
||
exit 1
|
||
}
|
||
Write-Host " ✓ 上传成功" -ForegroundColor Green
|
||
Write-Host ""
|
||
|
||
# 步骤 4: 在服务器上部署
|
||
Write-Host "[4/5] 在服务器上部署..." -ForegroundColor Yellow
|
||
|
||
# 创建本地临时部署脚本文件
|
||
$localDeployScript = Join-Path $env:TEMP "deploy-$timestamp.sh"
|
||
|
||
# 从模板文件读取 Bash 脚本内容
|
||
# 模板文件应该在脚本目录中,优先使用项目根目录的相对路径
|
||
$projectRoot = Get-Location
|
||
$templatePath = Join-Path $projectRoot "scripts\deploy\deploy-remote.sh.template"
|
||
# 如果找不到,尝试从脚本路径获取
|
||
if (-not (Test-Path $templatePath)) {
|
||
$scriptPath = $MyInvocation.MyCommand.Path
|
||
if ($scriptPath -and (Test-Path $scriptPath)) {
|
||
$scriptDir = Split-Path -Parent $scriptPath
|
||
$templatePath = Join-Path $scriptDir "deploy-remote.sh.template"
|
||
}
|
||
}
|
||
if (Test-Path $templatePath) {
|
||
$bashScriptTemplate = Get-Content -Path $templatePath -Raw -Encoding UTF8
|
||
} else {
|
||
Write-Host " ✗ 未找到部署脚本模板: $templatePath" -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
|
||
# 替换占位符
|
||
$bashScript = $bashScriptTemplate -replace "PROJECT_PATH_PLACEHOLDER", $ProjectPath
|
||
$bashScript = $bashScript -replace "REMOTE_FILE_NAME_PLACEHOLDER", $remoteFileName
|
||
|
||
# 确保使用 Unix 换行符(移除所有 \r,只保留 \n)
|
||
$bashScript = $bashScript -replace "`r`n", "`n" -replace "`r", "`n"
|
||
|
||
# 写入本地临时文件(使用 UTF8 无 BOM 编码)
|
||
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
||
[System.IO.File]::WriteAllText($localDeployScript, $bashScript, $utf8NoBom)
|
||
|
||
# 上传部署脚本到服务器
|
||
$remoteDeployScript = "/tmp/deploy-$timestamp.sh"
|
||
if ($usePlink) {
|
||
# 获取服务器密钥(如果之前没有获取)
|
||
if (-not $hostKey) {
|
||
$hostKey = Get-ServerHostKey -Server $Server
|
||
}
|
||
if ($hostKey) {
|
||
& $pscpPath -pw $Password -batch -hostkey $hostKey $localDeployScript "${User}@${Server}:$remoteDeployScript" 2>&1 | Out-Null
|
||
} else {
|
||
& $pscpPath -pw $Password -batch $localDeployScript "${User}@${Server}:$remoteDeployScript" 2>&1 | Out-Null
|
||
}
|
||
} else {
|
||
scp $localDeployScript "${User}@${Server}:$remoteDeployScript" 2>&1 | Out-Null
|
||
}
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Host " ✗ 上传部署脚本失败" -ForegroundColor Red
|
||
Remove-Item $localDeployScript -Force -ErrorAction SilentlyContinue
|
||
exit 1
|
||
}
|
||
|
||
# 在服务器上执行部署脚本
|
||
if ($usePlink) {
|
||
# 获取服务器密钥(如果之前没有获取)
|
||
if (-not $hostKey) {
|
||
$hostKey = Get-ServerHostKey -Server $Server
|
||
}
|
||
if ($hostKey) {
|
||
& $plinkPath -ssh -pw $Password -batch -hostkey $hostKey "${User}@${Server}" "chmod +x $remoteDeployScript; bash $remoteDeployScript; rm -f $remoteDeployScript" 2>&1
|
||
} else {
|
||
& $plinkPath -ssh -pw $Password -batch "${User}@${Server}" "chmod +x $remoteDeployScript; bash $remoteDeployScript; rm -f $remoteDeployScript" 2>&1
|
||
}
|
||
} else {
|
||
ssh "${User}@${Server}" "chmod +x $remoteDeployScript; bash $remoteDeployScript; rm -f $remoteDeployScript" 2>&1
|
||
}
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Host " ✗ 服务器部署失败" -ForegroundColor Red
|
||
Remove-Item $localDeployScript -Force -ErrorAction SilentlyContinue
|
||
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
|
||
exit 1
|
||
}
|
||
|
||
# 清理本地临时文件
|
||
Remove-Item $localDeployScript -Force -ErrorAction SilentlyContinue
|
||
Write-Host " ✓ 服务器部署成功" -ForegroundColor Green
|
||
Write-Host ""
|
||
|
||
# 步骤 5: 清理本地文件
|
||
Write-Host "[5/5] 清理本地文件..." -ForegroundColor Yellow
|
||
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
|
||
Write-Host " ✓ 清理完成" -ForegroundColor Green
|
||
Write-Host ""
|
||
|
||
# 部署完成
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host "部署完成!" -ForegroundColor Green
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host ""
|
||
Write-Host "下一步:" -ForegroundColor Yellow
|
||
Write-Host " 1. 访问网站检查是否正常" -ForegroundColor White
|
||
Write-Host " 2. 测试关键功能" -ForegroundColor White
|
||
Write-Host " 3. 检查错误日志" -ForegroundColor White
|
||
$logCheckCmd = "ssh ${User}@${Server} 'cd ${ProjectPath}; docker compose logs app --tail=100'"
|
||
Write-Host " $logCheckCmd" -ForegroundColor Gray
|
||
Write-Host ""
|
||
|