操作路径:宝塔面板 - 计划任务 - 添加任务
网站备份保存目录:
/www/backup/siteweb_wslink_20260408_013241_xPFlaL.tar.gz目录备份保存目录:
/www/backup/pathpath_wwwroot_20260408_012648_rJZMSG.tar.gz数据库备份保存目录:
/www/backup/database/mysql/crontab_backupwslink_2026-04-08_01-26-44_mysql_data.sql.gzNode + SFTP + schtasks
路径:宝塔面板 - 安全 - SSH管理
SSH服务:开启
SSH基础设置:
Root设置:
BT-Backupd/
bt-sync/
|-- backup.js
|-- jsconfig.json
|-- package.json
|-- package-lock.json
|-- certificate/
| `-- 180.131.145.178_id_ed25519
`-- node_modules/
目录说明
BT-Backupd
备份保存目录backup.js
主脚本。负责建立 SFTP 连接、递归列出远程目录并下载文件。certificate/
存放 SSH 私钥。当前脚本会读取 certificate/180.131.145.178_id_ed25519。jsconfig.json
JS 项目配置,已关闭 JS 文件的 TypeScript 校验。package.json
项目依赖和脚本配置。node_modules/
已安装的运行依赖。1、进入bt-sync
npm init -y
npm install ssh2-sftp-client
2、创建脚本 backup.js
// backup.js
const Client = require("ssh2-sftp-client");
const fs = require("fs");
const path = require("path");
const sftp = new Client();
const config = {
host: "你的服务器ip",
port: 22,
username: "root",
privateKey: fs.readFileSync(
path.join(__dirname, "certificate", "你的服务器ip_id_ed25519")
),
};
const remoteDir = "/www/backup/";
const backupRootDir = path.join(__dirname, "..", "BT-Backups");
// 保留的历史备份数量,超过这个数量的备份将被删除。设置为 0 或负数表示不删除任何备份。
const retentionCount = 5;
// 备份时要包含的路径列表(相对于 remoteDir)。
// 如果为空,则下载整个 remoteDir。
const includePaths = [
"database",
"path",
"site",
];
function formatBackupDirName(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
}
const currentBackupDir = path.join(
backupRootDir,
formatBackupDirName(new Date())
);
const logFile = path.join(currentBackupDir, "backup.log");
function log(message) {
const line = `[${new Date().toLocaleString()}] ${message}\n`;
fs.appendFileSync(logFile, line, "utf8");
console.log(message);
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function cleanupOldBackups() {
if (retentionCount <= 0) {
log("已跳过历史备份清理,retentionCount 小于等于 0");
return;
}
const backupDirs = fs
.readdirSync(backupRootDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort((a, b) => b.localeCompare(a, "en"));
const expiredDirs = backupDirs.slice(retentionCount);
for (const dirName of expiredDirs) {
const targetDir = path.join(backupRootDir, dirName);
if (path.dirname(targetDir) !== backupRootDir) {
log(`跳过异常目录: ${targetDir}`);
continue;
}
fs.rmSync(targetDir, { recursive: true, force: true });
log(`已删除旧备份: ${targetDir}`);
}
}
async function downloadDir(remotePath, localPath) {
ensureDir(localPath);
const items = await sftp.list(remotePath);
for (const item of items) {
const remoteItem = remotePath.endsWith("/")
? remotePath + item.name
: remotePath + "/" + item.name;
const localItem = path.join(localPath, item.name);
if (item.type === "d") {
await downloadDir(remoteItem, localItem);
} else {
log(`下载文件: ${remoteItem} -> ${localItem}`);
await sftp.fastGet(remoteItem, localItem);
}
}
}
function normalizeRemotePath(targetPath) {
return targetPath.replace(/\\/g, "/").replace(/\/+$/, "");
}
function resolveRemotePath(targetPath) {
const normalizedBase = normalizeRemotePath(remoteDir);
const normalizedTarget = normalizeRemotePath(targetPath);
if (normalizedTarget.startsWith("/")) {
return normalizedTarget;
}
return `${normalizedBase}/${normalizedTarget}`;
}
function resolveLocalPath(remotePath) {
const normalizedBase = `${normalizeRemotePath(remoteDir)}/`;
const normalizedRemotePath = normalizeRemotePath(remotePath);
const relativePath = normalizedRemotePath.startsWith(normalizedBase)
? normalizedRemotePath.slice(normalizedBase.length)
: path.posix.basename(normalizedRemotePath);
return path.join(currentBackupDir, ...relativePath.split("/"));
}
async function downloadTarget(targetPath) {
const remotePath = resolveRemotePath(targetPath);
const localPath = resolveLocalPath(remotePath);
const exists = await sftp.exists(remotePath);
if (!exists) {
log(`跳过不存在的路径: ${remotePath}`);
return;
}
if (exists === "d") {
log(`下载目录: ${remotePath} -> ${localPath}`);
await downloadDir(remotePath, localPath);
return;
}
ensureDir(path.dirname(localPath));
log(`下载文件: ${remotePath} -> ${localPath}`);
await sftp.fastGet(remotePath, localPath);
}
async function main() {
ensureDir(backupRootDir);
ensureDir(currentBackupDir);
try {
log("开始连接服务器...");
await sftp.connect(config);
if (includePaths.length > 0) {
log(`开始按白名单同步,共 ${includePaths.length} 项`);
for (const includePath of includePaths) {
await downloadTarget(includePath);
}
} else {
log("开始全量同步目录...");
await downloadDir(remoteDir, currentBackupDir);
}
log(`同步完成,本地目录: ${currentBackupDir}`);
cleanupOldBackups();
} catch (err) {
log(`发生错误: ${err.message}`);
process.exitCode = 1;
} finally {
await sftp.end();
}
}
main();
node backup.js
如果需要定时自动备份,可以根据操作系统选择对应方式。
可以使用“任务计划程序”定时执行:
nodebackup.js
c:\Users\Admin\Desktop\kailiyu\backups\bt-sync
也可以使用命令行创建计划任务:
schtasks /create /sc daily /st 02:00 /tn "bt-sync-backup" /tr "node c:\Users\Admin\Desktop\kailiyu\backups\bt-sync\backup.js"
可以使用 cron 定时执行。
编辑当前用户的定时任务:
crontab -e
例如每天凌晨 2 点执行一次:
0 2 * * * cd /path/to/backups/bt-sync && /usr/bin/node backup.js
说明:
/path/to/backups/bt-sync 替换为实际项目目录/usr/bin/node 替换为实际 Node.js 路径可以使用 launchd 定时执行。
示例 plist 文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.bt-sync.backup</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/path/to/backups/bt-sync/backup.js</string>
</array>
<key>WorkingDirectory</key>
<string>/path/to/backups/bt-sync</string>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
保存到:
~/Library/LaunchAgents/com.bt-sync.backup.plist
加载任务:
launchctl load ~/Library/LaunchAgents/com.bt-sync.backup.plist
node backup.js,确认脚本可正常运行node 的绝对路径BT-Backups 的权限