@shellus/way 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -24
- package/dist/cli.js +96 -51
- package/migrate-to-v0.5.sh +117 -0
- package/package.json +6 -3
- package/rules.yaml.example +28 -13
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
## 版本说明
|
|
8
8
|
|
|
9
|
-
- **v0.
|
|
9
|
+
- **v0.5.0+**: daemon 模式,支持项目级调度配置
|
|
10
|
+
- **v0.4.0**: TypeScript 版本,使用 systemd timer,凭证直接写在 YAML
|
|
10
11
|
- **v0.3.x**: TypeScript 版本,使用 crontab,支持 .env
|
|
11
12
|
- **v0.2.x**: Bash 版本
|
|
12
13
|
|
|
@@ -46,10 +47,11 @@
|
|
|
46
47
|
|
|
47
48
|
假设本机被入侵,攻击者不应能通过本机凭证定位或删除备份数据。
|
|
48
49
|
|
|
49
|
-
**v0.
|
|
50
|
-
- 使用
|
|
51
|
-
-
|
|
52
|
-
-
|
|
50
|
+
**v0.5.0 架构升级**:
|
|
51
|
+
- 使用 daemon 模式替代 systemd timer
|
|
52
|
+
- 支持项目级 schedule 配置(不同项目不同频率)
|
|
53
|
+
- 使用 node-cron 实现精确到分钟的调度
|
|
54
|
+
- systemd service 管理常驻进程(自动重启)
|
|
53
55
|
|
|
54
56
|
### 9. 多层冗余原则
|
|
55
57
|
|
|
@@ -119,15 +121,20 @@ way snapshots # 查看快照
|
|
|
119
121
|
|
|
120
122
|
```bash
|
|
121
123
|
# 备份命令
|
|
122
|
-
way run #
|
|
124
|
+
way run # 执行所有项目备份
|
|
123
125
|
way run data # 只备份 data 项目
|
|
126
|
+
way run data config # 备份多个项目
|
|
124
127
|
way gc # 按 retention 策略清理旧快照
|
|
128
|
+
way gc --dry-run # 模拟清理(不实际删除)
|
|
125
129
|
|
|
126
|
-
#
|
|
127
|
-
way
|
|
130
|
+
# daemon 模式(推荐)
|
|
131
|
+
way daemon # 启动常驻进程,按配置定时执行
|
|
132
|
+
|
|
133
|
+
# systemd 管理
|
|
134
|
+
way systemd install # 安装 systemd service(运行 daemon)
|
|
128
135
|
way systemd show # 显示 systemd 配置
|
|
129
|
-
way systemd status #
|
|
130
|
-
way systemd uninstall #
|
|
136
|
+
way systemd status # 查看服务状态
|
|
137
|
+
way systemd uninstall # 卸载服务
|
|
131
138
|
|
|
132
139
|
# 透传 restic(way 只设置环境变量)
|
|
133
140
|
way snapshots # → restic snapshots
|
|
@@ -143,16 +150,14 @@ way --remote=oss snapshots
|
|
|
143
150
|
|
|
144
151
|
```mermaid
|
|
145
152
|
graph LR
|
|
146
|
-
A[way
|
|
147
|
-
B --> C[
|
|
148
|
-
C --> D[
|
|
149
|
-
D --> E[
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
G -->
|
|
153
|
-
|
|
154
|
-
H[way gc] --> I[restic forget]
|
|
155
|
-
I --> J[restic prune]
|
|
153
|
+
A[way daemon] --> B[读取配置]
|
|
154
|
+
B --> C[为每个项目创建 cron 任务]
|
|
155
|
+
C --> D[node-cron 定时触发]
|
|
156
|
+
D --> E[执行 restic backup]
|
|
157
|
+
E --> F[推送 Uptime Kuma]
|
|
158
|
+
|
|
159
|
+
G[systemd service] --> H[启动 daemon]
|
|
160
|
+
H --> I[进程崩溃自动重启]
|
|
156
161
|
```
|
|
157
162
|
|
|
158
163
|
---
|
|
@@ -174,13 +179,36 @@ WAY_DIR=/path/to/config way snapshots
|
|
|
174
179
|
|
|
175
180
|
### rules.yaml
|
|
176
181
|
|
|
177
|
-
备份规则配置,参考 [`rules.yaml`](rules.yaml):
|
|
182
|
+
备份规则配置,参考 [`rules.yaml.example`](rules.yaml.example):
|
|
178
183
|
|
|
179
|
-
- **
|
|
180
|
-
- **
|
|
181
|
-
- **
|
|
184
|
+
- **defaults**: 全局默认配置(schedule、retention)
|
|
185
|
+
- **projects**: 备份项目配置,可覆盖默认 schedule 和 retention
|
|
186
|
+
- **maintenance**: 维护任务配置(prune、check)
|
|
182
187
|
- **global_excludes**: 全局排除规则
|
|
183
188
|
|
|
189
|
+
**项目级调度示例**:
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
defaults:
|
|
193
|
+
schedule: "0 */2 * * *" # 默认每 2 小时
|
|
194
|
+
|
|
195
|
+
projects:
|
|
196
|
+
data:
|
|
197
|
+
paths: [/data]
|
|
198
|
+
# 继承默认 schedule
|
|
199
|
+
|
|
200
|
+
logs:
|
|
201
|
+
paths: [/var/log]
|
|
202
|
+
schedule: "0 3 * * *" # 覆盖为每天凌晨 3 点
|
|
203
|
+
|
|
204
|
+
archive:
|
|
205
|
+
paths: [/archive]
|
|
206
|
+
schedule: "0 2 * * 0" # 每周日凌晨 2 点
|
|
207
|
+
retention:
|
|
208
|
+
keep_weekly: 8
|
|
209
|
+
keep_monthly: 12
|
|
210
|
+
```
|
|
211
|
+
|
|
184
212
|
#### 排除规则通配符语法
|
|
185
213
|
|
|
186
214
|
restic 使用 Go 的 filepath.Match 语法:
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,9 @@ function loadConfig(wayDir, remoteName) {
|
|
|
15
15
|
if (!repository) throw new Error(`Repository not found: ${repoName}`);
|
|
16
16
|
const rulesFile = path.join(wayDir, "rules.yaml");
|
|
17
17
|
const rules = yaml.load(fs.readFileSync(rulesFile, "utf8"));
|
|
18
|
+
if ("schedule" in rules && "backup" in (rules.schedule || {})) {
|
|
19
|
+
throw new Error("\u65E7\u914D\u7F6E\u683C\u5F0F\u4E0D\u518D\u652F\u6301\uFF0C\u8BF7\u53C2\u8003 rules.yaml.example \u66F4\u65B0\u914D\u7F6E");
|
|
20
|
+
}
|
|
18
21
|
return { repository, rules };
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -72,7 +75,7 @@ async function run(options) {
|
|
|
72
75
|
const env = buildResticEnv(config.repository);
|
|
73
76
|
const s3Options = buildS3Options(config.repository);
|
|
74
77
|
const globalExcludes = config.rules.global_excludes || [];
|
|
75
|
-
const projects = options.
|
|
78
|
+
const projects = options.projects && options.projects.length > 0 ? options.projects : Object.keys(config.rules.projects);
|
|
76
79
|
const succeeded = [];
|
|
77
80
|
const failed = [];
|
|
78
81
|
const startTime = Date.now();
|
|
@@ -123,11 +126,13 @@ async function notifyUptimeKuma(result, pushUrl) {
|
|
|
123
126
|
async function gc(options) {
|
|
124
127
|
const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
|
|
125
128
|
const config = loadConfig(wayDir, options.remote);
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
+
const retention = config.rules.defaults?.retention || {};
|
|
130
|
+
const keepDaily = retention.keep_daily || 7;
|
|
131
|
+
const keepWeekly = retention.keep_weekly || 4;
|
|
132
|
+
const keepMonthly = retention.keep_monthly || 6;
|
|
133
|
+
const keepYearly = retention.keep_yearly;
|
|
129
134
|
console.log("=== Cleaning snapshots ===");
|
|
130
|
-
console.log(`Policy: daily=${keepDaily}, weekly=${keepWeekly}, monthly=${keepMonthly}`);
|
|
135
|
+
console.log(`Policy: daily=${keepDaily}, weekly=${keepWeekly}, monthly=${keepMonthly}${keepYearly ? `, yearly=${keepYearly}` : ""}`);
|
|
131
136
|
const env = buildResticEnv(config.repository);
|
|
132
137
|
const s3Options = buildS3Options(config.repository);
|
|
133
138
|
const args = [
|
|
@@ -137,7 +142,8 @@ async function gc(options) {
|
|
|
137
142
|
`--keep-weekly=${keepWeekly}`,
|
|
138
143
|
`--keep-monthly=${keepMonthly}`
|
|
139
144
|
];
|
|
140
|
-
if (
|
|
145
|
+
if (keepYearly) args.push(`--keep-yearly=${keepYearly}`);
|
|
146
|
+
if (options.dryRun) args.push("--dry-run");
|
|
141
147
|
await execRestic(args, env, s3Options);
|
|
142
148
|
}
|
|
143
149
|
|
|
@@ -145,80 +151,122 @@ async function gc(options) {
|
|
|
145
151
|
import { execSync } from "child_process";
|
|
146
152
|
import fs2 from "fs";
|
|
147
153
|
import path2 from "path";
|
|
148
|
-
function cronToSystemd(cron) {
|
|
149
|
-
const parts = cron.trim().split(/\s+/);
|
|
150
|
-
if (parts.length !== 5) return "daily";
|
|
151
|
-
const [minute, hour, day, month, weekday] = parts;
|
|
152
|
-
if (day === "*" && month === "*" && weekday === "*") {
|
|
153
|
-
return `*-*-* ${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:00`;
|
|
154
|
-
}
|
|
155
|
-
return "daily";
|
|
156
|
-
}
|
|
157
154
|
async function systemd(options) {
|
|
158
155
|
const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
|
|
159
156
|
const config = loadConfig(wayDir, options.remote);
|
|
160
157
|
const wayPath = execSync("which way", { encoding: "utf-8" }).trim();
|
|
161
158
|
const serviceContent = `[Unit]
|
|
162
|
-
Description=Way Backup
|
|
159
|
+
Description=Way Backup Daemon
|
|
163
160
|
After=network.target
|
|
164
161
|
|
|
165
162
|
[Service]
|
|
166
|
-
Type=
|
|
167
|
-
ExecStart=${wayPath}
|
|
163
|
+
Type=simple
|
|
164
|
+
ExecStart=${wayPath} daemon
|
|
165
|
+
Restart=always
|
|
166
|
+
RestartSec=10
|
|
168
167
|
Environment="WAY_DIR=${wayDir}"
|
|
169
|
-
`;
|
|
170
|
-
const timerContent = `[Unit]
|
|
171
|
-
Description=Way Backup Timer
|
|
172
|
-
|
|
173
|
-
[Timer]
|
|
174
|
-
OnCalendar=${cronToSystemd(config.rules.schedule?.backup?.[0] || "daily")}
|
|
175
|
-
Persistent=true
|
|
176
168
|
|
|
177
169
|
[Install]
|
|
178
|
-
WantedBy=
|
|
170
|
+
WantedBy=default.target
|
|
179
171
|
`;
|
|
180
172
|
if (options.action === "show") {
|
|
181
173
|
console.log("=== way-backup.service ===");
|
|
182
174
|
console.log(serviceContent);
|
|
183
|
-
console.log("\n=== way-backup.timer ===");
|
|
184
|
-
console.log(timerContent);
|
|
185
175
|
return;
|
|
186
176
|
}
|
|
187
177
|
const systemdDir = `${process.env.HOME}/.config/systemd/user`;
|
|
188
178
|
const servicePath = path2.join(systemdDir, "way-backup.service");
|
|
189
|
-
const timerPath = path2.join(systemdDir, "way-backup.timer");
|
|
190
179
|
if (options.action === "install") {
|
|
191
180
|
fs2.mkdirSync(systemdDir, { recursive: true });
|
|
192
181
|
fs2.writeFileSync(servicePath, serviceContent);
|
|
193
|
-
fs2.writeFileSync(timerPath, timerContent);
|
|
194
182
|
execSync("systemctl --user daemon-reload");
|
|
195
|
-
execSync("systemctl --user enable way-backup.
|
|
196
|
-
execSync("systemctl --user start way-backup.
|
|
197
|
-
console.log("Systemd
|
|
198
|
-
execSync("systemctl --user --no-pager status way-backup.
|
|
183
|
+
execSync("systemctl --user enable way-backup.service");
|
|
184
|
+
execSync("systemctl --user start way-backup.service");
|
|
185
|
+
console.log("Systemd service installed and started");
|
|
186
|
+
execSync("systemctl --user --no-pager status way-backup.service", { stdio: "inherit" });
|
|
199
187
|
}
|
|
200
188
|
if (options.action === "uninstall") {
|
|
201
189
|
try {
|
|
202
|
-
execSync("systemctl --user stop way-backup.
|
|
190
|
+
execSync("systemctl --user stop way-backup.service", { stdio: "ignore" });
|
|
203
191
|
} catch {
|
|
204
192
|
}
|
|
205
193
|
try {
|
|
206
|
-
execSync("systemctl --user disable way-backup.
|
|
194
|
+
execSync("systemctl --user disable way-backup.service", { stdio: "ignore" });
|
|
207
195
|
} catch {
|
|
208
196
|
}
|
|
209
197
|
if (fs2.existsSync(servicePath)) fs2.unlinkSync(servicePath);
|
|
210
|
-
if (fs2.existsSync(timerPath)) fs2.unlinkSync(timerPath);
|
|
211
198
|
execSync("systemctl --user daemon-reload");
|
|
212
|
-
console.log("Systemd
|
|
199
|
+
console.log("Systemd service uninstalled");
|
|
213
200
|
}
|
|
214
201
|
if (options.action === "status") {
|
|
215
|
-
execSync("systemctl --user --no-pager status way-backup.
|
|
202
|
+
execSync("systemctl --user --no-pager status way-backup.service", { stdio: "inherit" });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/commands/daemon.ts
|
|
207
|
+
import cron from "node-cron";
|
|
208
|
+
var isRunning = false;
|
|
209
|
+
var taskQueue = [];
|
|
210
|
+
async function executeTask(task) {
|
|
211
|
+
taskQueue.push(task);
|
|
212
|
+
if (isRunning) return;
|
|
213
|
+
while (taskQueue.length > 0) {
|
|
214
|
+
isRunning = true;
|
|
215
|
+
const nextTask = taskQueue.shift();
|
|
216
|
+
try {
|
|
217
|
+
await nextTask();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error("Task failed:", error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
isRunning = false;
|
|
223
|
+
}
|
|
224
|
+
async function daemon(options) {
|
|
225
|
+
const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
|
|
226
|
+
const config = loadConfig(wayDir, options.remote);
|
|
227
|
+
console.log("Way daemon started");
|
|
228
|
+
for (const [name, project] of Object.entries(config.rules.projects)) {
|
|
229
|
+
const schedule = project.schedule || config.rules.defaults?.schedule || "0 */2 * * *";
|
|
230
|
+
cron.schedule(schedule, () => {
|
|
231
|
+
executeTask(async () => {
|
|
232
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Running backup: ${name}`);
|
|
233
|
+
await run({ remote: options.remote, projects: [name] });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
console.log(`Scheduled backup for ${name}: ${schedule}`);
|
|
216
237
|
}
|
|
238
|
+
const pruneSchedule = config.rules.maintenance?.prune?.schedule;
|
|
239
|
+
if (pruneSchedule) {
|
|
240
|
+
cron.schedule(pruneSchedule, () => {
|
|
241
|
+
executeTask(async () => {
|
|
242
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Running prune`);
|
|
243
|
+
await gc({ remote: options.remote, dryRun: false });
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
console.log(`Scheduled prune: ${pruneSchedule}`);
|
|
247
|
+
}
|
|
248
|
+
const checkSchedule = config.rules.maintenance?.check?.schedule;
|
|
249
|
+
if (checkSchedule) {
|
|
250
|
+
cron.schedule(checkSchedule, () => {
|
|
251
|
+
executeTask(async () => {
|
|
252
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Running check`);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
console.log(`Scheduled check: ${checkSchedule}`);
|
|
256
|
+
}
|
|
257
|
+
process.on("SIGTERM", () => {
|
|
258
|
+
console.log("Received SIGTERM, shutting down gracefully");
|
|
259
|
+
process.exit(0);
|
|
260
|
+
});
|
|
261
|
+
process.on("SIGINT", () => {
|
|
262
|
+
console.log("Received SIGINT, shutting down gracefully");
|
|
263
|
+
process.exit(0);
|
|
264
|
+
});
|
|
217
265
|
}
|
|
218
266
|
|
|
219
267
|
// src/cli.ts
|
|
220
268
|
var program = new Command();
|
|
221
|
-
program.name("way").version("0.
|
|
269
|
+
program.name("way").version("0.5.0").description("\u7B56\u7565\u5907\u4EFD\u5DE5\u5177 - \u57FA\u4E8E restic \u7684\u7B56\u7565\u5C01\u88C5").option("--remote <name>", "\u6307\u5B9A\u4ED3\u5E93", "default").addHelpText("after", `
|
|
222
270
|
\u793A\u4F8B:
|
|
223
271
|
$ way run \u6267\u884C\u6240\u6709\u9879\u76EE\u5907\u4EFD
|
|
224
272
|
$ way run data \u53EA\u5907\u4EFD data \u9879\u76EE
|
|
@@ -230,25 +278,22 @@ program.name("way").version("0.4.0").description("\u7B56\u7565\u5907\u4EFD\u5DE5
|
|
|
230
278
|
|
|
231
279
|
\u6587\u6863: https://github.com/shellus/way
|
|
232
280
|
`);
|
|
233
|
-
program.command("run [
|
|
281
|
+
program.command("run [projects...]").description("\u6267\u884C\u5907\u4EFD").allowUnknownOption().allowExcessArguments().action(async (projects, options, command) => {
|
|
234
282
|
const remote = command.parent.opts().remote;
|
|
235
|
-
|
|
236
|
-
if (project?.startsWith("--")) {
|
|
237
|
-
await run({ remote, project: void 0, extraArgs: allArgs });
|
|
238
|
-
} else {
|
|
239
|
-
const extraArgs = project ? allArgs.slice(1) : allArgs;
|
|
240
|
-
await run({ remote, project, extraArgs });
|
|
241
|
-
}
|
|
283
|
+
await run({ remote, projects });
|
|
242
284
|
});
|
|
243
|
-
program.command("gc").description("\u6E05\u7406\u65E7\u5FEB\u7167").
|
|
285
|
+
program.command("gc").description("\u6E05\u7406\u65E7\u5FEB\u7167").option("--dry-run", "\u6A21\u62DF\u6E05\u7406\uFF08\u4E0D\u5B9E\u9645\u5220\u9664\uFF09").action(async function(cmdOptions) {
|
|
244
286
|
const remote = this.parent.opts().remote;
|
|
245
|
-
|
|
246
|
-
await gc({ remote, extraArgs });
|
|
287
|
+
await gc({ remote, dryRun: cmdOptions.dryRun });
|
|
247
288
|
});
|
|
248
289
|
program.command("systemd <action>").description("\u7BA1\u7406 systemd \u5B9A\u65F6\u4EFB\u52A1 (show|install|uninstall|status)").action(async (action, options, command) => {
|
|
249
290
|
const remote = command.parent.opts().remote;
|
|
250
291
|
await systemd({ remote, action });
|
|
251
292
|
});
|
|
293
|
+
program.command("daemon").description("\u542F\u52A8\u5E38\u9A7B\u8FDB\u7A0B\uFF0C\u6309\u914D\u7F6E\u5B9A\u65F6\u6267\u884C\u5907\u4EFD").action(async (options, command) => {
|
|
294
|
+
const remote = command.parent.opts().remote;
|
|
295
|
+
await daemon({ remote });
|
|
296
|
+
});
|
|
252
297
|
program.command("env").description("\u663E\u793A\u73AF\u5883\u53D8\u91CF").action(() => {
|
|
253
298
|
const env = Object.entries(process.env).sort(([a], [b]) => a.localeCompare(b));
|
|
254
299
|
for (const [key, value] of env) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 迁移脚本:v0.4.x -> v0.5.0
|
|
3
|
+
# 将旧配置格式转换为新格式
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
WAY_DIR="${WAY_DIR:-$HOME/.way}"
|
|
8
|
+
RULES_FILE="$WAY_DIR/rules.yaml"
|
|
9
|
+
BACKUP_FILE="$WAY_DIR/rules.yaml.v0.4.backup"
|
|
10
|
+
|
|
11
|
+
if [ ! -f "$RULES_FILE" ]; then
|
|
12
|
+
echo "错误: 未找到 $RULES_FILE"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# 备份原配置
|
|
17
|
+
cp "$RULES_FILE" "$BACKUP_FILE"
|
|
18
|
+
echo "已备份原配置到: $BACKUP_FILE"
|
|
19
|
+
|
|
20
|
+
# 检查是否是旧格式
|
|
21
|
+
if ! grep -q "^schedule:" "$RULES_FILE"; then
|
|
22
|
+
echo "配置已是新格式,无需迁移"
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# 提取旧配置的值
|
|
27
|
+
OLD_SCHEDULE=$(grep -A2 "^schedule:" "$BACKUP_FILE" | grep -A1 "backup:" | tail -1 | sed 's/.*"\(.*\)".*/\1/')
|
|
28
|
+
OLD_PRUNE=$(grep -A3 "^schedule:" "$BACKUP_FILE" | grep "prune:" | sed 's/.*"\(.*\)".*/\1/')
|
|
29
|
+
OLD_CHECK=$(grep -A4 "^schedule:" "$BACKUP_FILE" | grep "check:" | sed 's/.*"\(.*\)".*/\1/')
|
|
30
|
+
|
|
31
|
+
KEEP_DAILY=$(grep "keep_daily:" "$BACKUP_FILE" | sed 's/.*: \(.*\)/\1/')
|
|
32
|
+
KEEP_WEEKLY=$(grep "keep_weekly:" "$BACKUP_FILE" | sed 's/.*: \(.*\)/\1/')
|
|
33
|
+
KEEP_MONTHLY=$(grep "keep_monthly:" "$BACKUP_FILE" | sed 's/.*: \(.*\)/\1/')
|
|
34
|
+
|
|
35
|
+
# 生成新配置
|
|
36
|
+
cat > "$RULES_FILE" << EOF
|
|
37
|
+
# 备份规则配置 (v0.5.0)
|
|
38
|
+
# 旧配置已备份到: $(basename $BACKUP_FILE)
|
|
39
|
+
|
|
40
|
+
# 全局默认配置
|
|
41
|
+
defaults:
|
|
42
|
+
schedule: "${OLD_SCHEDULE:-0 */2 * * *}"
|
|
43
|
+
retention:
|
|
44
|
+
keep_daily: ${KEEP_DAILY:-7}
|
|
45
|
+
keep_weekly: ${KEEP_WEEKLY:-4}
|
|
46
|
+
keep_monthly: ${KEEP_MONTHLY:-6}
|
|
47
|
+
|
|
48
|
+
EOF
|
|
49
|
+
|
|
50
|
+
# 复制 uptime_kuma 配置
|
|
51
|
+
if grep -q "^uptime_kuma:" "$BACKUP_FILE"; then
|
|
52
|
+
echo "# Uptime Kuma 通知" >> "$RULES_FILE"
|
|
53
|
+
grep -A1 "^uptime_kuma:" "$BACKUP_FILE" >> "$RULES_FILE"
|
|
54
|
+
echo "" >> "$RULES_FILE"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# 复制 projects 配置
|
|
58
|
+
echo "# 备份项目" >> "$RULES_FILE"
|
|
59
|
+
sed -n '/^projects:/,/^global_excludes:/p' "$BACKUP_FILE" | sed '$d' >> "$RULES_FILE"
|
|
60
|
+
echo "" >> "$RULES_FILE"
|
|
61
|
+
|
|
62
|
+
# 复制 global_excludes
|
|
63
|
+
sed -n '/^global_excludes:/,$p' "$BACKUP_FILE" >> "$RULES_FILE"
|
|
64
|
+
|
|
65
|
+
# 添加 maintenance 配置
|
|
66
|
+
if [ -n "$OLD_PRUNE" ] || [ -n "$OLD_CHECK" ]; then
|
|
67
|
+
echo "" >> "$RULES_FILE"
|
|
68
|
+
echo "# 维护任务" >> "$RULES_FILE"
|
|
69
|
+
echo "maintenance:" >> "$RULES_FILE"
|
|
70
|
+
if [ -n "$OLD_PRUNE" ]; then
|
|
71
|
+
echo " prune:" >> "$RULES_FILE"
|
|
72
|
+
echo " schedule: \"$OLD_PRUNE\"" >> "$RULES_FILE"
|
|
73
|
+
fi
|
|
74
|
+
if [ -n "$OLD_CHECK" ]; then
|
|
75
|
+
echo " check:" >> "$RULES_FILE"
|
|
76
|
+
echo " schedule: \"$OLD_CHECK\"" >> "$RULES_FILE"
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
echo "✓ 配置迁移完成"
|
|
81
|
+
|
|
82
|
+
# 检测并升级 systemd 服务
|
|
83
|
+
TIMER_FILE="$HOME/.config/systemd/user/way-backup.timer"
|
|
84
|
+
SERVICE_FILE="$HOME/.config/systemd/user/way-backup.service"
|
|
85
|
+
|
|
86
|
+
if systemctl --user is-active way-backup.timer &>/dev/null || [ -f "$TIMER_FILE" ]; then
|
|
87
|
+
echo ""
|
|
88
|
+
echo "检测到旧版 systemd timer,正在升级为 daemon service..."
|
|
89
|
+
|
|
90
|
+
# 停止并禁用旧 timer
|
|
91
|
+
systemctl --user stop way-backup.timer 2>/dev/null || true
|
|
92
|
+
systemctl --user disable way-backup.timer 2>/dev/null || true
|
|
93
|
+
rm -f "$TIMER_FILE"
|
|
94
|
+
|
|
95
|
+
# 停止旧 service(如果在运行)
|
|
96
|
+
systemctl --user stop way-backup.service 2>/dev/null || true
|
|
97
|
+
|
|
98
|
+
# 重新安装新版 service
|
|
99
|
+
way systemd install
|
|
100
|
+
|
|
101
|
+
echo "✓ systemd 服务已从 timer 升级为 daemon"
|
|
102
|
+
elif systemctl --user is-active way-backup.service &>/dev/null || [ -f "$SERVICE_FILE" ]; then
|
|
103
|
+
echo ""
|
|
104
|
+
echo "检测到 systemd service,正在重新安装..."
|
|
105
|
+
|
|
106
|
+
# 重新安装以更新配置
|
|
107
|
+
way systemd uninstall
|
|
108
|
+
way systemd install
|
|
109
|
+
|
|
110
|
+
echo "✓ systemd 服务已更新"
|
|
111
|
+
else
|
|
112
|
+
echo ""
|
|
113
|
+
echo "未检测到 systemd 服务,如需安装: way systemd install"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
echo ""
|
|
117
|
+
echo "如需回滚配置: mv $BACKUP_FILE $RULES_FILE"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shellus/way",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "将备份作为持续运营的项目,而非一次性任务。基于 restic 的策略封装。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"repositories.yaml.example",
|
|
19
|
-
"rules.yaml.example"
|
|
19
|
+
"rules.yaml.example",
|
|
20
|
+
"migrate-to-v0.5.sh"
|
|
20
21
|
],
|
|
21
22
|
"repository": {
|
|
22
23
|
"type": "git",
|
|
@@ -43,11 +44,13 @@
|
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"commander": "^14.0.3",
|
|
45
46
|
"execa": "^9.6.1",
|
|
46
|
-
"js-yaml": "^4.1.1"
|
|
47
|
+
"js-yaml": "^4.1.1",
|
|
48
|
+
"node-cron": "^4.2.1"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
51
|
"@types/js-yaml": "^4.0.9",
|
|
50
52
|
"@types/node": "^25.5.0",
|
|
53
|
+
"@types/node-cron": "^3.0.11",
|
|
51
54
|
"tsup": "^8.5.1",
|
|
52
55
|
"typescript": "^5.9.3",
|
|
53
56
|
"vitest": "^4.1.0"
|
package/rules.yaml.example
CHANGED
|
@@ -2,23 +2,19 @@
|
|
|
2
2
|
# 备份目的地见 repositories.yaml
|
|
3
3
|
# 复制为 rules.yaml 并根据实际情况修改
|
|
4
4
|
|
|
5
|
+
# 全局默认配置
|
|
6
|
+
defaults:
|
|
7
|
+
schedule: "0 */2 * * *" # 默认每 2 小时
|
|
8
|
+
retention:
|
|
9
|
+
keep_daily: 7
|
|
10
|
+
keep_weekly: 4
|
|
11
|
+
keep_monthly: 6
|
|
12
|
+
|
|
5
13
|
# Uptime Kuma 通知(备份完成后推送状态)
|
|
6
14
|
uptime_kuma:
|
|
7
15
|
push_url: "https://uptime.example.com/api/push/xxxxx"
|
|
8
16
|
|
|
9
|
-
#
|
|
10
|
-
schedule:
|
|
11
|
-
backup:
|
|
12
|
-
- "0 12 * * *" # 每天中午 12:00
|
|
13
|
-
prune: "0 4 * * 0" # 每周日凌晨 4:00 清理
|
|
14
|
-
check: "" # 完整性检查,留空不执行
|
|
15
|
-
|
|
16
|
-
# 快照保留策略
|
|
17
|
-
retention:
|
|
18
|
-
keep_daily: 7
|
|
19
|
-
keep_weekly: 4
|
|
20
|
-
keep_monthly: 6
|
|
21
|
-
|
|
17
|
+
# 备份项目
|
|
22
18
|
projects:
|
|
23
19
|
data:
|
|
24
20
|
description: 数据目录
|
|
@@ -33,6 +29,18 @@ projects:
|
|
|
33
29
|
paths:
|
|
34
30
|
- /home/user/.config
|
|
35
31
|
- /home/user/.ssh
|
|
32
|
+
schedule: "0 3 * * *" # 覆盖默认,每天凌晨 3 点
|
|
33
|
+
excludes: []
|
|
34
|
+
|
|
35
|
+
archive:
|
|
36
|
+
description: 历史归档
|
|
37
|
+
paths:
|
|
38
|
+
- /archive
|
|
39
|
+
schedule: "0 2 * * 0" # 每周日凌晨 2 点
|
|
40
|
+
retention:
|
|
41
|
+
keep_weekly: 8
|
|
42
|
+
keep_monthly: 12
|
|
43
|
+
keep_yearly: 3
|
|
36
44
|
excludes: []
|
|
37
45
|
|
|
38
46
|
# 全局排除规则 - 适用于所有项目
|
|
@@ -60,3 +68,10 @@ global_excludes:
|
|
|
60
68
|
- "*.log"
|
|
61
69
|
- "*.tmp"
|
|
62
70
|
- "*.swp"
|
|
71
|
+
|
|
72
|
+
# 维护任务(可选)
|
|
73
|
+
maintenance:
|
|
74
|
+
prune:
|
|
75
|
+
schedule: "0 4 * * 0" # 每周日凌晨 4 点清理
|
|
76
|
+
check:
|
|
77
|
+
schedule: "" # 留空不执行
|