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

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

概述#

对于使用 Astro、Hugo、Hexo 等静态博客框架的用户,在本地 pnpm build 后需要把 dist 目录上传到服务器。相比手动用 FTP/宝塔面板拖拽,WinSCP 命令行版的优势在于:

  • ✅ 增量同步:只传有变化的文件
  • ✅ 哈希校验:-criteria=checksum 确保文件传输完整
  • ✅ 镜像模式:-delete 自动删除服务器多余文件
  • ✅ 纯命令行:可以封装成 .ps1 脚本一键执行
  • ✅ 兼容宝塔:Windows 用户最熟悉的 GUI + 命令行双模式工具

本文给出完整的 WinSCP + SFTP 部署流程,包含两套脚本(单步同步、构建+同步一键)和所有避坑点。


一、安装 WinSCP(命令行版)#

WinSCP 安装包同时包含 GUI 和 命令行(WinSCP.com),我们只需要两个文件:

  1. WinSCP 官网下载页 下载「Portable executables」便携版
  2. 解压后将以下 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

Terminal window
<#
.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 Green
pause

保存注意:PowerShell 脚本里有中文时,必须用「UTF-8 with BOM」编码保存(用 VSCode 右下角选「带 BOM 的 UTF-8」即可),否则会出现「锟斤拷」乱码导致脚本解析失败。


四、构建 + 同步 一键脚本#

适合场景:写完文章后一条命令发布到底。

tools/ 下新建 构建并同步.ps1

Terminal window
<#
.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 $projectRoot
try {
& 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 Magenta
Write-Host " 构建 :$(if ($buildExitCode -eq 0) { '成功' } else { '失败' }) 耗时 $($buildTimer.Elapsed.ToString('mm\分ss\秒'))" -ForegroundColor Magenta
Write-Host " 同步 :$(if ($syncExitCode -eq 0) { '成功' } else { "失败($syncExitCode)" }) 耗时 $($syncTimer.Elapsed.ToString('mm\分ss\秒'))" -ForegroundColor Magenta
Write-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)解析,中文字符被解析错位,破坏了引号和花括号的配对。

解决(二选一):

  1. VSCode 右下角编码选择 → 通过编码保存UTF-8 with BOM(推荐)
  2. 干脆脚本里所有注释、提示全部用英文,永不乱码

坑 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 内部命令,执行时会:

  1. 发送 SSH 断开消息关闭 SFTP 会话
  2. 关闭 TCP 连接
  3. 退出 WinSCP.com 进程

就算没写 exit,只要脚本跑完进程退出,操作系统也会自动回收 socket。无需额外处理。


六、WinSCP vs rclone 方案对比#

两者都能搞定 SFTP 部署,按习惯选一个即可:

对比项WinSCPrclone
并发传输默认单线程,调参较麻烦--transfers 8 轻松并发
增量策略支持 size/time/checksum支持 + --size-only 快速模式
GUI 配套官方自带 GUI,连不上服务器可以开 GUI 排错纯命令行,需第三方 GUI
Windows 上手难度低(老牌 Windows 用户都认识)中(需命令行思维+配置文件)
跨平台仅 WindowsWindows/macOS/Linux 全平台
配置方式直接把参数写脚本里rclone config 生成 rclone.conf
发布速度一般(无并发时较慢)(并发 + --size-only 跳过 mtime 坑)

结论

  • 已经有 rclone 经验的 → 继续用 rclone
  • 纯 Windows 用户、习惯图形工具、偶尔需要手动传文件的 → WinSCP 方案更顺手

七、安全建议(进阶)#

脚本里明文写 SSH 密码虽然方便,但如果要把项目推到公共 Git 仓库(Gitee/GitHub),务必把包含密码的 .ps1 脚本加入 .gitignore

.gitignore
tools/*.ps1
tools/sync-log.txt
tools/winscp_tmp.txt
tools/WinSCP.*

更安全的做法(推荐后续升级):

  1. 改用 SSH 密钥登录ssh-keygen 生成密钥对,公钥放服务器 authorized_keys
  2. WinSCP 里用 -privatekey="C:\Users\你\.ssh\id_ed25519.ppk" 代替密码
  3. 脚本里不再出现任何敏感信息,可放心提交到 Git

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

静态博客一键发布到服务器:WinSCP + SFTP 实战指南
https://www.cpdd520.top/posts/winscp-sftp-deploy/
作者
小鼠宝
发布于
2026-08-09
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
小鼠宝
来和我一起打洲吧!
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
最新动态
站点统计
文章
6
分类
3
标签
22
总字数
7,264
运行时长
0
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.15.6
文章许可
CC BY-NC-SA 4.0