@shellus/way 0.4.2 → 0.5.1
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 +61 -24
- package/dist/cli.js +105 -56
- package/migrate-to-v0.5.sh +117 -0
- package/package.json +6 -3
- package/rules.yaml.example +33 -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,45 @@ 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
|
+
|
|
212
|
+
**schedule 语法**(node-cron 格式):
|
|
213
|
+
|
|
214
|
+
| 格式 | 说明 | 示例 |
|
|
215
|
+
|------|------|------|
|
|
216
|
+
| `"0 */2 * * *"` | 间隔表达式 | 每 2 小时 |
|
|
217
|
+
| `"0 9,15,21 * * *"` | 多个时间点(逗号) | 每天 9:00、15:00、21:00 |
|
|
218
|
+
| `"0 9-17 * * 1-5"` | 时间范围 | 工作日 9:00-17:00 每小时 |
|
|
219
|
+
| `"*/30 * * * *"` | 分钟间隔 | 每 30 分钟 |
|
|
220
|
+
|
|
184
221
|
#### 排除规则通配符语法
|
|
185
222
|
|
|
186
223
|
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("\u68C0\u6D4B\u5230 v0.4.x \u65E7\u914D\u7F6E\u683C\u5F0F\uFF0C\u8BF7\u8FD0\u884C\u8FC1\u79FB\u811A\u672C\uFF1Anpx --yes @shellus/way@latest migrate-to-v0.5.sh \u6216\u624B\u52A8\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,124 @@ 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();
|
|
158
|
+
const currentUser = execSync("whoami", { encoding: "utf-8" }).trim();
|
|
161
159
|
const serviceContent = `[Unit]
|
|
162
|
-
Description=Way Backup
|
|
160
|
+
Description=Way Backup Daemon
|
|
163
161
|
After=network.target
|
|
164
162
|
|
|
165
163
|
[Service]
|
|
166
|
-
Type=
|
|
167
|
-
|
|
164
|
+
Type=simple
|
|
165
|
+
User=${currentUser}
|
|
166
|
+
ExecStart=${wayPath} daemon
|
|
167
|
+
Restart=always
|
|
168
|
+
RestartSec=10
|
|
168
169
|
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
|
|
170
|
+
Environment="HOME=${process.env.HOME}"
|
|
176
171
|
|
|
177
172
|
[Install]
|
|
178
|
-
WantedBy=
|
|
173
|
+
WantedBy=multi-user.target
|
|
179
174
|
`;
|
|
180
175
|
if (options.action === "show") {
|
|
181
176
|
console.log("=== way-backup.service ===");
|
|
182
177
|
console.log(serviceContent);
|
|
183
|
-
console.log("\n=== way-backup.timer ===");
|
|
184
|
-
console.log(timerContent);
|
|
185
178
|
return;
|
|
186
179
|
}
|
|
187
|
-
const systemdDir =
|
|
180
|
+
const systemdDir = "/etc/systemd/system";
|
|
188
181
|
const servicePath = path2.join(systemdDir, "way-backup.service");
|
|
189
|
-
const timerPath = path2.join(systemdDir, "way-backup.timer");
|
|
190
182
|
if (options.action === "install") {
|
|
191
|
-
fs2.mkdirSync(systemdDir, { recursive: true });
|
|
192
183
|
fs2.writeFileSync(servicePath, serviceContent);
|
|
193
|
-
|
|
194
|
-
execSync("systemctl
|
|
195
|
-
execSync("systemctl
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
execSync("systemctl --user --no-pager status way-backup.timer", { stdio: "inherit" });
|
|
184
|
+
execSync("systemctl daemon-reload");
|
|
185
|
+
execSync("systemctl enable way-backup.service");
|
|
186
|
+
execSync("systemctl start way-backup.service");
|
|
187
|
+
console.log("Systemd service installed and started");
|
|
188
|
+
execSync("systemctl --no-pager status way-backup.service", { stdio: "inherit" });
|
|
199
189
|
}
|
|
200
190
|
if (options.action === "uninstall") {
|
|
201
191
|
try {
|
|
202
|
-
execSync("systemctl
|
|
192
|
+
execSync("systemctl stop way-backup.service", { stdio: "ignore" });
|
|
203
193
|
} catch {
|
|
204
194
|
}
|
|
205
195
|
try {
|
|
206
|
-
execSync("systemctl
|
|
196
|
+
execSync("systemctl disable way-backup.service", { stdio: "ignore" });
|
|
207
197
|
} catch {
|
|
208
198
|
}
|
|
209
199
|
if (fs2.existsSync(servicePath)) fs2.unlinkSync(servicePath);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.log("Systemd timer uninstalled");
|
|
200
|
+
execSync("systemctl daemon-reload");
|
|
201
|
+
console.log("Systemd service uninstalled");
|
|
213
202
|
}
|
|
214
203
|
if (options.action === "status") {
|
|
215
|
-
execSync("systemctl --
|
|
204
|
+
execSync("systemctl --no-pager status way-backup.service", { stdio: "inherit" });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/commands/daemon.ts
|
|
209
|
+
import cron from "node-cron";
|
|
210
|
+
var isRunning = false;
|
|
211
|
+
var taskQueue = [];
|
|
212
|
+
async function executeTask(task) {
|
|
213
|
+
taskQueue.push(task);
|
|
214
|
+
if (isRunning) return;
|
|
215
|
+
while (taskQueue.length > 0) {
|
|
216
|
+
isRunning = true;
|
|
217
|
+
const nextTask = taskQueue.shift();
|
|
218
|
+
try {
|
|
219
|
+
await nextTask();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error("Task failed:", error);
|
|
222
|
+
}
|
|
216
223
|
}
|
|
224
|
+
isRunning = false;
|
|
225
|
+
}
|
|
226
|
+
async function daemon(options) {
|
|
227
|
+
const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
|
|
228
|
+
const config = loadConfig(wayDir, options.remote);
|
|
229
|
+
console.log("Way daemon started");
|
|
230
|
+
for (const [name, project] of Object.entries(config.rules.projects)) {
|
|
231
|
+
const schedule = project.schedule || config.rules.defaults?.schedule || "0 */2 * * *";
|
|
232
|
+
cron.schedule(schedule, () => {
|
|
233
|
+
executeTask(async () => {
|
|
234
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Running backup: ${name}`);
|
|
235
|
+
await run({ remote: options.remote, projects: [name] });
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
console.log(`Scheduled backup for ${name}: ${schedule}`);
|
|
239
|
+
}
|
|
240
|
+
const pruneSchedule = config.rules.maintenance?.prune?.schedule;
|
|
241
|
+
if (pruneSchedule) {
|
|
242
|
+
cron.schedule(pruneSchedule, () => {
|
|
243
|
+
executeTask(async () => {
|
|
244
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Running prune`);
|
|
245
|
+
await gc({ remote: options.remote, dryRun: false });
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
console.log(`Scheduled prune: ${pruneSchedule}`);
|
|
249
|
+
}
|
|
250
|
+
const checkSchedule = config.rules.maintenance?.check?.schedule;
|
|
251
|
+
if (checkSchedule) {
|
|
252
|
+
cron.schedule(checkSchedule, () => {
|
|
253
|
+
executeTask(async () => {
|
|
254
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Running check`);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
console.log(`Scheduled check: ${checkSchedule}`);
|
|
258
|
+
}
|
|
259
|
+
process.on("SIGTERM", () => {
|
|
260
|
+
console.log("Received SIGTERM, shutting down gracefully");
|
|
261
|
+
process.exit(0);
|
|
262
|
+
});
|
|
263
|
+
process.on("SIGINT", () => {
|
|
264
|
+
console.log("Received SIGINT, shutting down gracefully");
|
|
265
|
+
process.exit(0);
|
|
266
|
+
});
|
|
217
267
|
}
|
|
218
268
|
|
|
219
269
|
// src/cli.ts
|
|
220
270
|
var program = new Command();
|
|
221
|
-
program.name("way").version("0.
|
|
271
|
+
program.name("way").version("0.5.1").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
272
|
\u793A\u4F8B:
|
|
223
273
|
$ way run \u6267\u884C\u6240\u6709\u9879\u76EE\u5907\u4EFD
|
|
224
274
|
$ way run data \u53EA\u5907\u4EFD data \u9879\u76EE
|
|
@@ -230,25 +280,24 @@ program.name("way").version("0.4.0").description("\u7B56\u7565\u5907\u4EFD\u5DE5
|
|
|
230
280
|
|
|
231
281
|
\u6587\u6863: https://github.com/shellus/way
|
|
232
282
|
`);
|
|
233
|
-
program.command("run [
|
|
234
|
-
const remote =
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
} else {
|
|
239
|
-
const extraArgs = project ? allArgs.slice(1) : allArgs;
|
|
240
|
-
await run({ remote, project, extraArgs });
|
|
241
|
-
}
|
|
283
|
+
program.command("run [projects...]").description("\u6267\u884C\u5907\u4EFD").option("--dry-run", "\u6A21\u62DF\u5907\u4EFD\uFF08\u4E0D\u5B9E\u9645\u5199\u5165\uFF09").allowUnknownOption().allowExcessArguments().action(async function(projects) {
|
|
284
|
+
const remote = this.parent.opts().remote;
|
|
285
|
+
const extraArgs = this.args.filter((a) => a.startsWith("-") && !["--dry-run"].includes(a));
|
|
286
|
+
if (this.opts().dryRun) extraArgs.push("--dry-run");
|
|
287
|
+
await run({ remote, projects: projects.filter((p) => !p.startsWith("-")), extraArgs });
|
|
242
288
|
});
|
|
243
|
-
program.command("gc").description("\u6E05\u7406\u65E7\u5FEB\u7167").
|
|
289
|
+
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
290
|
const remote = this.parent.opts().remote;
|
|
245
|
-
|
|
246
|
-
await gc({ remote, extraArgs });
|
|
291
|
+
await gc({ remote, dryRun: cmdOptions.dryRun });
|
|
247
292
|
});
|
|
248
293
|
program.command("systemd <action>").description("\u7BA1\u7406 systemd \u5B9A\u65F6\u4EFB\u52A1 (show|install|uninstall|status)").action(async (action, options, command) => {
|
|
249
294
|
const remote = command.parent.opts().remote;
|
|
250
295
|
await systemd({ remote, action });
|
|
251
296
|
});
|
|
297
|
+
program.command("daemon").description("\u542F\u52A8\u5E38\u9A7B\u8FDB\u7A0B\uFF0C\u6309\u914D\u7F6E\u5B9A\u65F6\u6267\u884C\u5907\u4EFD").action(async (options, command) => {
|
|
298
|
+
const remote = command.parent.opts().remote;
|
|
299
|
+
await daemon({ remote });
|
|
300
|
+
});
|
|
252
301
|
program.command("env").description("\u663E\u793A\u73AF\u5883\u53D8\u91CF").action(() => {
|
|
253
302
|
const env = Object.entries(process.env).sort(([a], [b]) => a.localeCompare(b));
|
|
254
303
|
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
|
+
USER_TIMER="$HOME/.config/systemd/user/way-backup.timer"
|
|
84
|
+
USER_SERVICE="$HOME/.config/systemd/user/way-backup.service"
|
|
85
|
+
SYSTEM_SERVICE="/etc/systemd/system/way-backup.service"
|
|
86
|
+
|
|
87
|
+
# 清理旧版 user 级别服务
|
|
88
|
+
if systemctl --user is-active way-backup.timer &>/dev/null || [ -f "$USER_TIMER" ]; then
|
|
89
|
+
echo ""
|
|
90
|
+
echo "检测到旧版 user 级 systemd 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 "$USER_TIMER"
|
|
94
|
+
systemctl --user stop way-backup.service 2>/dev/null || true
|
|
95
|
+
systemctl --user disable way-backup.service 2>/dev/null || true
|
|
96
|
+
rm -f "$USER_SERVICE"
|
|
97
|
+
systemctl --user daemon-reload
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if systemctl --user is-active way-backup.service &>/dev/null || [ -f "$USER_SERVICE" ]; then
|
|
101
|
+
echo ""
|
|
102
|
+
echo "检测到旧版 user 级 systemd service,正在清理..."
|
|
103
|
+
systemctl --user stop way-backup.service 2>/dev/null || true
|
|
104
|
+
systemctl --user disable way-backup.service 2>/dev/null || true
|
|
105
|
+
rm -f "$USER_SERVICE"
|
|
106
|
+
systemctl --user daemon-reload
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# 安装 system 级别服务
|
|
110
|
+
echo ""
|
|
111
|
+
echo "安装 system 级 systemd service..."
|
|
112
|
+
way systemd install
|
|
113
|
+
|
|
114
|
+
echo "✓ systemd 服务已升级为 system 级别"
|
|
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.1",
|
|
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,24 @@
|
|
|
2
2
|
# 备份目的地见 repositories.yaml
|
|
3
3
|
# 复制为 rules.yaml 并根据实际情况修改
|
|
4
4
|
|
|
5
|
+
# 全局默认配置
|
|
6
|
+
defaults:
|
|
7
|
+
# schedule 使用 node-cron 语法(分 时 日 月 周)
|
|
8
|
+
# 支持多种表达方式:
|
|
9
|
+
# "0 */2 * * *" - 每 2 小时
|
|
10
|
+
# "0 9,15,21 * * *" - 每天 9:00、15:00、21:00
|
|
11
|
+
# "0 9-17 * * 1-5" - 工作日 9:00-17:00 每小时
|
|
12
|
+
schedule: "0 */2 * * *"
|
|
13
|
+
retention:
|
|
14
|
+
keep_daily: 7
|
|
15
|
+
keep_weekly: 4
|
|
16
|
+
keep_monthly: 6
|
|
17
|
+
|
|
5
18
|
# Uptime Kuma 通知(备份完成后推送状态)
|
|
6
19
|
uptime_kuma:
|
|
7
20
|
push_url: "https://uptime.example.com/api/push/xxxxx"
|
|
8
21
|
|
|
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
|
-
|
|
22
|
+
# 备份项目
|
|
22
23
|
projects:
|
|
23
24
|
data:
|
|
24
25
|
description: 数据目录
|
|
@@ -33,6 +34,18 @@ projects:
|
|
|
33
34
|
paths:
|
|
34
35
|
- /home/user/.config
|
|
35
36
|
- /home/user/.ssh
|
|
37
|
+
schedule: "0 3 * * *" # 覆盖默认,每天凌晨 3 点
|
|
38
|
+
excludes: []
|
|
39
|
+
|
|
40
|
+
archive:
|
|
41
|
+
description: 历史归档
|
|
42
|
+
paths:
|
|
43
|
+
- /archive
|
|
44
|
+
schedule: "0 2 * * 0" # 每周日凌晨 2 点
|
|
45
|
+
retention:
|
|
46
|
+
keep_weekly: 8
|
|
47
|
+
keep_monthly: 12
|
|
48
|
+
keep_yearly: 3
|
|
36
49
|
excludes: []
|
|
37
50
|
|
|
38
51
|
# 全局排除规则 - 适用于所有项目
|
|
@@ -60,3 +73,10 @@ global_excludes:
|
|
|
60
73
|
- "*.log"
|
|
61
74
|
- "*.tmp"
|
|
62
75
|
- "*.swp"
|
|
76
|
+
|
|
77
|
+
# 维护任务(可选)
|
|
78
|
+
maintenance:
|
|
79
|
+
prune:
|
|
80
|
+
schedule: "0 4 * * 0" # 每周日凌晨 4 点清理
|
|
81
|
+
check:
|
|
82
|
+
schedule: "" # 留空不执行
|