静态博客一键发布到服务器:WinSCP + SFTP 实战指南

概述
对于使用 Astro、Hugo、Hexo 等静态博客框架的用户,在本地 pnpm build 后需要把 dist 目录上传到服务器。相比手动用 FTP/宝塔面板拖拽,WinSCP 命令行版的优势在于:
- ✅ 增量同步:只传有变化的文件
- ✅ 哈希校验:
-criteria=checksum确保文件传输完整 - ✅ 镜像模式:
-delete自动删除服务器多余文件 - ✅ 纯命令行:可以封装成
.ps1脚本一键执行 - ✅ 兼容宝塔:Windows 用户最熟悉的 GUI + 命令行双模式工具
本文给出完整的 WinSCP + SFTP 部署流程,包含两套脚本(单步同步、构建+同步一键)和所有避坑点。
一、安装 WinSCP(命令行版)
WinSCP 安装包同时包含 GUI 和 命令行(WinSCP.com),我们只需要两个文件:
- 去 WinSCP 官网下载页 下载「Portable executables」便携版
- 解压后将以下 3 个文件放到博客项目的
tools/目录下:
your-blog/└── tools/ ├── WinSCP.com ← 命令行主程序(我们用这个) ├── WinSCP.exe ← GUI 版本(依赖,必须一起放) └── WinSCP.chs ← 简体中文语言包(可选,放了命令行会输出中文)💡 不用配环境变量,脚本里用
$PSScriptRoot取同目录路径,直接相对位置调用即可。
二、同步方向(最容易踩坑的点)
WinSCP synchronize 命令的方向参数有两个,写反会被覆盖:
| 命令 | 方向 | 含义 |
|---|---|---|
synchronize local | 远程 → 本地 | 用服务器的文件覆盖本地 ❌(删除本地多余文件) |
synchronize remote | 本地 → 远程 | 用本地 dist 覆盖服务器 ✅(删除服务器多余文件) |
写博客部署用的一定是 synchronize remote。如果不小心写成 local,服务器上旧版本的文件会把你刚构建好的新 dist 覆盖回来。
快速验证方法:加 -preview 参数只预览不执行,看输出里是「新建本地文件」还是「新建远程文件」:
# 预览命令,不会实际执行synchronize remote "本地路径" "远程路径" -preview三、单步同步脚本(只做同步)
适合场景:已经 pnpm build 完,只想把 dist 推上去。
在 tools/ 下新建 同步到服务器.ps1:
<#.SYNOPSIS 静态博客 dist 一键同步到服务器.DESCRIPTION WinSCP SFTP 增量同步:上传新增/修改文件 + 删除服务器多余文件#>
# ========== 配置区(改成你自己的信息) ==========$winscpPath = Join-Path $PSScriptRoot "WinSCP.com"$localDir = "D:\your-blog\dist\" # 改:本地 dist 路径$remoteDir = "/www/wwwroot/blog" # 改:服务器站点目录$sshHost = "你的服务器IP" # 改:例如 123.45.67.89$port = 22 # SSH 端口,一般默认 22$user = "root" # 改:SSH 用户名$pass = "你的SSH密码" # 改:SSH 密码$logFile = Join-Path $PSScriptRoot "sync-log.txt"
# ========== 前置检查 ==========if (-not (Test-Path $winscpPath)) { Write-Host "[错误] 未找到 WinSCP.com,请放在 tools\ 目录下。" -ForegroundColor Red pause exit 1}
# ========== 核心同步命令 ==========# 参数说明:# -hostkey=* 跳过主机指纹确认(首次连接不卡手动确认)# synchronize remote 本地 -> 服务器# -delete 删除服务器多余文件(镜像模式)# -criteria=checksum 用哈希判断文件是否变化(比时间戳准)# -filemask="|.user.ini;**/.user.ini" 排除所有目录下的 .user.ini# |.user.ini = 排除根目录 .user.ini# **/.user.ini = 排除所有子目录 .user.ini(宝塔面板会生成)
$scriptText = @"open sftp://$($user):$($pass)@$($sshHost):$port -hostkey=*synchronize remote "$localDir" "$remoteDir" -delete -criteria=checksum -filemask="|.user.ini;**/.user.ini"exit"@
$tmpScript = Join-Path $PSScriptRoot "winscp_tmp.txt"[System.IO.File]::WriteAllText($tmpScript, $scriptText, [System.Text.Encoding]::ASCII)
Write-Host "[信息] 开始同步..." -ForegroundColor Cyan& $winscpPath /script="$tmpScript" /log="$logFile"
Remove-Item $tmpScript -ErrorAction SilentlyContinue
Write-Host ""Write-Host "[完成] 同步完毕,日志:$logFile" -ForegroundColor Greenpause保存注意:PowerShell 脚本里有中文时,必须用「UTF-8 with BOM」编码保存(用 VSCode 右下角选「带 BOM 的 UTF-8」即可),否则会出现「锟斤拷」乱码导致脚本解析失败。
四、构建 + 同步 一键脚本
适合场景:写完文章后一条命令发布到底。
在 tools/ 下新建 构建并同步.ps1:
<#.SYNOPSIS 一键:pnpm build -> WinSCP SFTP 同步 构建失败则不会执行同步,避免把空 dist 推到服务器#>
# ========== 路径 ==========$projectRoot = Split-Path $PSScriptRoot -Parent # 项目根目录(tools 上一级)$distDir = Join-Path $projectRoot "dist"$winscpPath = Join-Path $PSScriptRoot "WinSCP.com"$logFile = Join-Path $PSScriptRoot "sync-log.txt"
# ========== 服务器连接配置(改成你自己的) ==========$sshHost = "你的服务器IP"$port = 22$user = "root"$pass = "你的SSH密码"$remoteDir = "/www/wwwroot/blog"
# ========== 环境检查 ==========try { $pnpmVer = & pnpm --version 2>$null if (-not $pnpmVer) { throw "pnpm not found" } Write-Host "[OK] pnpm v$pnpmVer" -ForegroundColor Gray} catch { Write-Host "[错误] 未检测到 pnpm,请先安装:npm install -g pnpm" -ForegroundColor Red pause exit 1}
# ========== Step 1:构建 ==========Write-Host ""Write-Host "[1/2] 开始构建:pnpm build" -ForegroundColor Cyan$buildTimer = [System.Diagnostics.Stopwatch]::StartNew()
Push-Location $projectRoottry { & pnpm build $buildExitCode = $LASTEXITCODE} finally { Pop-Location}
$buildTimer.Stop()
if ($buildExitCode -ne 0) { Write-Host "" Write-Host "[错误] 构建失败(退出码 $buildExitCode),已中止同步。" -ForegroundColor Red pause exit 1}Write-Host "[OK] 构建成功,耗时 $($buildTimer.Elapsed.ToString('mm\分ss\秒'))" -ForegroundColor Green
# ========== Step 2:同步 ==========Write-Host ""Write-Host "[2/2] 开始同步 dist -> 服务器 $remoteDir" -ForegroundColor Cyan$syncTimer = [System.Diagnostics.Stopwatch]::StartNew()
$winscpCmd = @"open sftp://$($user):$($pass)@$($sshHost):$port -hostkey=*synchronize remote "$distDir\" "$remoteDir" -delete -criteria=checksum -filemask="|.user.ini;**/.user.ini"exit"@
$tmp = Join-Path $PSScriptRoot "winscp_tmp.txt"[System.IO.File]::WriteAllText($tmp, $winscpCmd, [System.Text.Encoding]::ASCII)
& $winscpPath /script="$tmp" /log="$logFile"$syncExitCode = $LASTEXITCODE
Remove-Item $tmp -ErrorAction SilentlyContinue$syncTimer.Stop()
# ========== 汇总报告 ==========Write-Host ""Write-Host "=============== 发布结果 ===============" -ForegroundColor MagentaWrite-Host " 构建 :$(if ($buildExitCode -eq 0) { '成功' } else { '失败' }) 耗时 $($buildTimer.Elapsed.ToString('mm\分ss\秒'))" -ForegroundColor MagentaWrite-Host " 同步 :$(if ($syncExitCode -eq 0) { '成功' } else { "失败($syncExitCode)" }) 耗时 $($syncTimer.Elapsed.ToString('mm\分ss\秒'))" -ForegroundColor MagentaWrite-Host "========================================" -ForegroundColor Magenta
if ($syncExitCode -ne 0) { Write-Host "[警告] 同步有问题,日志:$logFile" -ForegroundColor Yellow}
Write-Host ""pause五、避坑指南
坑 1:同步方向写反(远程覆盖本地)
症状:本地刚 build 的新文件不见了,变回服务器上的旧版本
解决:务必使用 synchronize remote(local = 把本地当成目标,remote = 把远程当成目标)。首次运行前加 -preview 预览确认。
坑 2:删除 .user.ini 报错 Permission denied
症状:
删除文件 '/www/wwwroot/blog/.user.ini' 时出错无权访问。错误码:3服务器返回的错误消息:Permission denied
原因:宝塔面板在站点根目录生成的 .user.ini 设置了文件锁(chattr +i),root 都删不掉。而且这个文件里存了 open_basedir 和防跨站设置,绝不能删。
解决:同步时用 -filemask="|.user.ini;**/.user.ini" 完整排除所有位置的 .user.ini。
注意排除语法:
- 前缀
|表示排除(不要写成!,WinSCP 的 filemask 语法用|排除) **/.user.ini匹配所有子目录(和 rclone 里的**.user.ini同理)- 整个 filemask 值必须加双引号,否则分号会被截断
坑 3:中文乱码 / 脚本解析报错
症状:PowerShell 执行时报错
The string is missing the terminator、锟斤拷、Unexpected token
原因:.ps1 文件保存成了 UTF-8 无 BOM,Windows PowerShell 5.x 默认按系统 ANSI(GBK/CP936)解析,中文字符被解析错位,破坏了引号和花括号的配对。
解决(二选一):
- VSCode 右下角编码选择 → 通过编码保存 → UTF-8 with BOM(推荐)
- 干脆脚本里所有注释、提示全部用英文,永不乱码
坑 4:-criteria 选 size 还是 checksum?
| 参数 | 速度 | 准确性 | 适用场景 |
|---|---|---|---|
-criteria=size | 最快(只看大小) | 低 | 只改内容不改大小的文件会漏同步 |
-criteria=time | 快(时间+大小) | 中 | Windows/Linux 时区不同导致误判(和 rclone 的 mtime 问题一样) |
-criteria=checksum | 略慢(算哈希) | 最高 | 博客部署推荐,绝不漏传 |
静态博客站点一般几百 MB 以内,checksum 多花几秒换绝对稳妥,值得。
坑 5:密码里有特殊字符被转义
如果 SSH 密码里有 @、:、/、#、? 等字符,直接拼在 sftp://user:pass@host URL 里会被解析成分隔符,导致连接失败。
解决:改用 -username / -password / -privatekey 单独参数(或直接改一个不含特殊字符的密码):
open sftp://$sshHost:$port -hostkey=* -username="$user" -password="$pass"坑 6:连接会自动关闭吗?
会的。脚本末尾的 exit 是 WinSCP 内部命令,执行时会:
- 发送 SSH 断开消息关闭 SFTP 会话
- 关闭 TCP 连接
- 退出
WinSCP.com进程
就算没写 exit,只要脚本跑完进程退出,操作系统也会自动回收 socket。无需额外处理。
六、WinSCP vs rclone 方案对比
两者都能搞定 SFTP 部署,按习惯选一个即可:
| 对比项 | WinSCP | rclone |
|---|---|---|
| 并发传输 | 默认单线程,调参较麻烦 | --transfers 8 轻松并发 |
| 增量策略 | 支持 size/time/checksum | 支持 + --size-only 快速模式 |
| GUI 配套 | 官方自带 GUI,连不上服务器可以开 GUI 排错 | 纯命令行,需第三方 GUI |
| Windows 上手难度 | 低(老牌 Windows 用户都认识) | 中(需命令行思维+配置文件) |
| 跨平台 | 仅 Windows | Windows/macOS/Linux 全平台 |
| 配置方式 | 直接把参数写脚本里 | rclone config 生成 rclone.conf |
| 发布速度 | 一般(无并发时较慢) | 快(并发 + --size-only 跳过 mtime 坑) |
结论:
- 已经有 rclone 经验的 → 继续用 rclone
- 纯 Windows 用户、习惯图形工具、偶尔需要手动传文件的 → WinSCP 方案更顺手
七、安全建议(进阶)
脚本里明文写 SSH 密码虽然方便,但如果要把项目推到公共 Git 仓库(Gitee/GitHub),务必把包含密码的 .ps1 脚本加入 .gitignore:
tools/*.ps1tools/sync-log.txttools/winscp_tmp.txttools/WinSCP.*更安全的做法(推荐后续升级):
- 改用 SSH 密钥登录(
ssh-keygen生成密钥对,公钥放服务器authorized_keys) - WinSCP 里用
-privatekey="C:\Users\你\.ssh\id_ed25519.ppk"代替密码 - 脚本里不再出现任何敏感信息,可放心提交到 Git
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!




