@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 CHANGED
@@ -6,7 +6,8 @@
6
6
 
7
7
  ## 版本说明
8
8
 
9
- - **v0.4.0+**: TypeScript 版本,使用 systemd,凭证直接写在 YAML
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.4.0 安全增强**:
50
- - 使用 systemd 替代 crontab(避免在 crontab 留下痕迹)
51
- - 移除 .env 文件支持(避免被 `find .env` 发现)
52
- - 凭证直接写在 `~/.way/repositories.yaml`(权限 600)
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 # 执行备份(读取 rules.yaml 的项目和排除规则)
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
- # systemd 定时任务
127
- way systemd install # 安装 systemd 定时任务
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 run] --> B[读取配置]
147
- B --> C[遍历项目]
148
- C --> D[执行 restic backup]
149
- D --> E[推送 Uptime Kuma]
150
-
151
- F[systemd timer] --> G[定时触发]
152
- G --> A
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
- - **projects**: 备份项目、路径、专属排除规则
180
- - **schedule**: 备份时间(systemd timer OnCalendar 格式)
181
- - **retention**: 快照保留策略
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.project ? [options.project] : Object.keys(config.rules.projects);
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 keepDaily = config.rules.retention?.keep_daily || 7;
127
- const keepWeekly = config.rules.retention?.keep_weekly || 4;
128
- const keepMonthly = config.rules.retention?.keep_monthly || 6;
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 (options.extraArgs) args.push(...options.extraArgs);
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 Service
159
+ Description=Way Backup Daemon
163
160
  After=network.target
164
161
 
165
162
  [Service]
166
- Type=oneshot
167
- ExecStart=${wayPath} run
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=timers.target
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.timer");
196
- execSync("systemctl --user start way-backup.timer");
197
- console.log("Systemd timer installed and started");
198
- execSync("systemctl --user --no-pager status way-backup.timer", { stdio: "inherit" });
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.timer", { stdio: "ignore" });
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.timer", { stdio: "ignore" });
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 timer uninstalled");
199
+ console.log("Systemd service uninstalled");
213
200
  }
214
201
  if (options.action === "status") {
215
- execSync("systemctl --user --no-pager status way-backup.timer", { stdio: "inherit" });
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.4.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", `
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 [project]").description("\u6267\u884C\u5907\u4EFD").allowUnknownOption().allowExcessArguments().action(async (project, options, command) => {
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
- const allArgs = command.parent.args.slice(1);
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").allowUnknownOption().allowExcessArguments().action(async function() {
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
- const extraArgs = this.parent.args.slice(1);
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.4.2",
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"
@@ -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
- # 备份时间(cron 格式,会自动转换为 systemd OnCalendar)
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: "" # 留空不执行