@shellus/way 0.5.0 → 0.5.2
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 +22 -1
- package/dist/cli.js +65 -26
- package/migrate-to-v0.5.sh +23 -23
- package/package.json +2 -4
- package/rules.yaml.example +6 -1
- package/vendor/restic/linux-x64/LICENSE +25 -0
- package/vendor/restic/linux-x64/VERSION +1 -0
- package/vendor/restic/linux-x64/restic +0 -0
package/README.md
CHANGED
|
@@ -67,8 +67,14 @@
|
|
|
67
67
|
|
|
68
68
|
## 依赖
|
|
69
69
|
|
|
70
|
-
- [restic](https://restic.net/) - 备份引擎
|
|
71
70
|
- Node.js >= 18
|
|
71
|
+
- Linux x64 平台内置 [restic](https://restic.net/) 0.16.4,其他平台需自行安装 restic
|
|
72
|
+
|
|
73
|
+
`way` 查找 restic 的顺序:
|
|
74
|
+
|
|
75
|
+
1. `WAY_RESTIC_BIN` 指定的二进制
|
|
76
|
+
2. Linux x64 包内置的 restic
|
|
77
|
+
3. 系统 `PATH` 中的 `restic`
|
|
72
78
|
|
|
73
79
|
## 安装
|
|
74
80
|
|
|
@@ -76,6 +82,12 @@
|
|
|
76
82
|
npm install -g @shellus/way
|
|
77
83
|
```
|
|
78
84
|
|
|
85
|
+
Linux x64 用户无需额外安装 restic。如需使用自定义 restic,可设置:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
WAY_RESTIC_BIN=/usr/local/bin/restic way snapshots
|
|
89
|
+
```
|
|
90
|
+
|
|
79
91
|
## 快速开始
|
|
80
92
|
|
|
81
93
|
### 1. 初始化配置
|
|
@@ -209,6 +221,15 @@ projects:
|
|
|
209
221
|
keep_monthly: 12
|
|
210
222
|
```
|
|
211
223
|
|
|
224
|
+
**schedule 语法**(node-cron 格式):
|
|
225
|
+
|
|
226
|
+
| 格式 | 说明 | 示例 |
|
|
227
|
+
|------|------|------|
|
|
228
|
+
| `"0 */2 * * *"` | 间隔表达式 | 每 2 小时 |
|
|
229
|
+
| `"0 9,15,21 * * *"` | 多个时间点(逗号) | 每天 9:00、15:00、21:00 |
|
|
230
|
+
| `"0 9-17 * * 1-5"` | 时间范围 | 工作日 9:00-17:00 每小时 |
|
|
231
|
+
| `"*/30 * * * *"` | 分钟间隔 | 每 30 分钟 |
|
|
232
|
+
|
|
212
233
|
#### 排除规则通配符语法
|
|
213
234
|
|
|
214
235
|
restic 使用 Go 的 filepath.Match 语法:
|
package/dist/cli.js
CHANGED
|
@@ -16,13 +16,45 @@ function loadConfig(wayDir, remoteName) {
|
|
|
16
16
|
const rulesFile = path.join(wayDir, "rules.yaml");
|
|
17
17
|
const rules = yaml.load(fs.readFileSync(rulesFile, "utf8"));
|
|
18
18
|
if ("schedule" in rules && "backup" in (rules.schedule || {})) {
|
|
19
|
-
throw new Error("\u65E7\u914D\u7F6E\u683C\u5F0F\
|
|
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
20
|
}
|
|
21
21
|
return { repository, rules };
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// src/core/restic.ts
|
|
25
25
|
import { execa } from "execa";
|
|
26
|
+
|
|
27
|
+
// src/core/restic-bin.ts
|
|
28
|
+
import fs2 from "fs";
|
|
29
|
+
import path2 from "path";
|
|
30
|
+
import { fileURLToPath } from "url";
|
|
31
|
+
function getBundledResticBin(packageRoot) {
|
|
32
|
+
return path2.join(packageRoot, "vendor/restic/linux-x64/restic");
|
|
33
|
+
}
|
|
34
|
+
function findPackageRoot(startDir = path2.dirname(fileURLToPath(import.meta.url))) {
|
|
35
|
+
let current = startDir;
|
|
36
|
+
while (true) {
|
|
37
|
+
if (fs2.existsSync(path2.join(current, "package.json"))) return current;
|
|
38
|
+
const parent = path2.dirname(current);
|
|
39
|
+
if (parent === current) return startDir;
|
|
40
|
+
current = parent;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function resolveResticBin(options = {}) {
|
|
44
|
+
const env = options.env ?? process.env;
|
|
45
|
+
const platform = options.platform ?? process.platform;
|
|
46
|
+
const arch = options.arch ?? process.arch;
|
|
47
|
+
const packageRoot = options.packageRoot ?? findPackageRoot();
|
|
48
|
+
const existsSync = options.existsSync ?? fs2.existsSync;
|
|
49
|
+
if (env.WAY_RESTIC_BIN) return env.WAY_RESTIC_BIN;
|
|
50
|
+
if (platform === "linux" && arch === "x64") {
|
|
51
|
+
const bundled = getBundledResticBin(packageRoot);
|
|
52
|
+
if (existsSync(bundled)) return bundled;
|
|
53
|
+
}
|
|
54
|
+
return "restic";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/core/restic.ts
|
|
26
58
|
function buildResticEnv(repo) {
|
|
27
59
|
const env = {};
|
|
28
60
|
switch (repo.type) {
|
|
@@ -57,11 +89,11 @@ function buildS3Options(repo) {
|
|
|
57
89
|
}
|
|
58
90
|
async function execRestic(args, env, s3Options = []) {
|
|
59
91
|
try {
|
|
60
|
-
await execa(
|
|
92
|
+
await execa(resolveResticBin(), [...s3Options, ...args], { env: { ...process.env, ...env }, stdio: "inherit" });
|
|
61
93
|
} catch (error) {
|
|
62
94
|
if (error.code === "ENOENT") {
|
|
63
|
-
console.error("Error: restic not found.
|
|
64
|
-
console.error("
|
|
95
|
+
console.error("Error: restic not found. Linux x64 packages include restic; other platforms must install it first.");
|
|
96
|
+
console.error("Set WAY_RESTIC_BIN to use a custom restic binary, or visit: https://restic.net/");
|
|
65
97
|
process.exit(1);
|
|
66
98
|
}
|
|
67
99
|
throw error;
|
|
@@ -76,6 +108,8 @@ async function run(options) {
|
|
|
76
108
|
const s3Options = buildS3Options(config.repository);
|
|
77
109
|
const globalExcludes = config.rules.global_excludes || [];
|
|
78
110
|
const projects = options.projects && options.projects.length > 0 ? options.projects : Object.keys(config.rules.projects);
|
|
111
|
+
const dryRun = options.dryRun || options.extraArgs?.includes("--dry-run") || false;
|
|
112
|
+
const extraArgs = options.extraArgs?.filter((arg) => arg !== "--dry-run") || [];
|
|
79
113
|
const succeeded = [];
|
|
80
114
|
const failed = [];
|
|
81
115
|
const startTime = Date.now();
|
|
@@ -89,7 +123,8 @@ async function run(options) {
|
|
|
89
123
|
console.log(`=== Backing up: ${projectName} ===`);
|
|
90
124
|
try {
|
|
91
125
|
const args = buildBackupArgs(projectName, project, globalExcludes);
|
|
92
|
-
if (
|
|
126
|
+
if (dryRun) args.push("--dry-run");
|
|
127
|
+
args.push(...extraArgs);
|
|
93
128
|
await execRestic(args, env, s3Options);
|
|
94
129
|
succeeded.push(projectName);
|
|
95
130
|
} catch (error) {
|
|
@@ -101,7 +136,7 @@ async function run(options) {
|
|
|
101
136
|
console.log("\n=== Summary ===");
|
|
102
137
|
if (succeeded.length > 0) console.log("Succeeded:", succeeded.join(", "));
|
|
103
138
|
if (failed.length > 0) console.log("Failed:", failed.join(", "));
|
|
104
|
-
if (config.rules.uptime_kuma?.push_url) {
|
|
139
|
+
if (!dryRun && config.rules.uptime_kuma?.push_url) {
|
|
105
140
|
await notifyUptimeKuma({ succeeded, failed, duration }, config.rules.uptime_kuma.push_url);
|
|
106
141
|
}
|
|
107
142
|
return { succeeded, failed, duration };
|
|
@@ -149,57 +184,59 @@ async function gc(options) {
|
|
|
149
184
|
|
|
150
185
|
// src/commands/systemd.ts
|
|
151
186
|
import { execSync } from "child_process";
|
|
152
|
-
import
|
|
153
|
-
import
|
|
187
|
+
import fs3 from "fs";
|
|
188
|
+
import path3 from "path";
|
|
154
189
|
async function systemd(options) {
|
|
155
190
|
const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
|
|
156
191
|
const config = loadConfig(wayDir, options.remote);
|
|
157
192
|
const wayPath = execSync("which way", { encoding: "utf-8" }).trim();
|
|
193
|
+
const currentUser = execSync("whoami", { encoding: "utf-8" }).trim();
|
|
158
194
|
const serviceContent = `[Unit]
|
|
159
195
|
Description=Way Backup Daemon
|
|
160
196
|
After=network.target
|
|
161
197
|
|
|
162
198
|
[Service]
|
|
163
199
|
Type=simple
|
|
200
|
+
User=${currentUser}
|
|
164
201
|
ExecStart=${wayPath} daemon
|
|
165
202
|
Restart=always
|
|
166
203
|
RestartSec=10
|
|
167
204
|
Environment="WAY_DIR=${wayDir}"
|
|
205
|
+
Environment="HOME=${process.env.HOME}"
|
|
168
206
|
|
|
169
207
|
[Install]
|
|
170
|
-
WantedBy=
|
|
208
|
+
WantedBy=multi-user.target
|
|
171
209
|
`;
|
|
172
210
|
if (options.action === "show") {
|
|
173
211
|
console.log("=== way-backup.service ===");
|
|
174
212
|
console.log(serviceContent);
|
|
175
213
|
return;
|
|
176
214
|
}
|
|
177
|
-
const systemdDir =
|
|
178
|
-
const servicePath =
|
|
215
|
+
const systemdDir = "/etc/systemd/system";
|
|
216
|
+
const servicePath = path3.join(systemdDir, "way-backup.service");
|
|
179
217
|
if (options.action === "install") {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
execSync("systemctl
|
|
183
|
-
execSync("systemctl
|
|
184
|
-
execSync("systemctl --user start way-backup.service");
|
|
218
|
+
fs3.writeFileSync(servicePath, serviceContent);
|
|
219
|
+
execSync("systemctl daemon-reload");
|
|
220
|
+
execSync("systemctl enable way-backup.service");
|
|
221
|
+
execSync("systemctl start way-backup.service");
|
|
185
222
|
console.log("Systemd service installed and started");
|
|
186
|
-
execSync("systemctl --
|
|
223
|
+
execSync("systemctl --no-pager status way-backup.service", { stdio: "inherit" });
|
|
187
224
|
}
|
|
188
225
|
if (options.action === "uninstall") {
|
|
189
226
|
try {
|
|
190
|
-
execSync("systemctl
|
|
227
|
+
execSync("systemctl stop way-backup.service", { stdio: "ignore" });
|
|
191
228
|
} catch {
|
|
192
229
|
}
|
|
193
230
|
try {
|
|
194
|
-
execSync("systemctl
|
|
231
|
+
execSync("systemctl disable way-backup.service", { stdio: "ignore" });
|
|
195
232
|
} catch {
|
|
196
233
|
}
|
|
197
|
-
if (
|
|
198
|
-
execSync("systemctl
|
|
234
|
+
if (fs3.existsSync(servicePath)) fs3.unlinkSync(servicePath);
|
|
235
|
+
execSync("systemctl daemon-reload");
|
|
199
236
|
console.log("Systemd service uninstalled");
|
|
200
237
|
}
|
|
201
238
|
if (options.action === "status") {
|
|
202
|
-
execSync("systemctl --
|
|
239
|
+
execSync("systemctl --no-pager status way-backup.service", { stdio: "inherit" });
|
|
203
240
|
}
|
|
204
241
|
}
|
|
205
242
|
|
|
@@ -266,7 +303,7 @@ async function daemon(options) {
|
|
|
266
303
|
|
|
267
304
|
// src/cli.ts
|
|
268
305
|
var program = new Command();
|
|
269
|
-
program.name("way").version("0.5.
|
|
306
|
+
program.name("way").version("0.5.2").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", `
|
|
270
307
|
\u793A\u4F8B:
|
|
271
308
|
$ way run \u6267\u884C\u6240\u6709\u9879\u76EE\u5907\u4EFD
|
|
272
309
|
$ way run data \u53EA\u5907\u4EFD data \u9879\u76EE
|
|
@@ -278,9 +315,11 @@ program.name("way").version("0.5.0").description("\u7B56\u7565\u5907\u4EFD\u5DE5
|
|
|
278
315
|
|
|
279
316
|
\u6587\u6863: https://github.com/shellus/way
|
|
280
317
|
`);
|
|
281
|
-
program.command("run [projects...]").description("\u6267\u884C\u5907\u4EFD").allowUnknownOption().allowExcessArguments().action(async (projects
|
|
282
|
-
const remote =
|
|
283
|
-
|
|
318
|
+
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) {
|
|
319
|
+
const remote = this.parent.opts().remote;
|
|
320
|
+
const dryRun = this.opts().dryRun;
|
|
321
|
+
const extraArgs = this.args.filter((a) => a.startsWith("-") && !["--dry-run"].includes(a));
|
|
322
|
+
await run({ remote, projects: projects.filter((p) => !p.startsWith("-")), extraArgs, dryRun });
|
|
284
323
|
});
|
|
285
324
|
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) {
|
|
286
325
|
const remote = this.parent.opts().remote;
|
package/migrate-to-v0.5.sh
CHANGED
|
@@ -80,38 +80,38 @@ fi
|
|
|
80
80
|
echo "✓ 配置迁移完成"
|
|
81
81
|
|
|
82
82
|
# 检测并升级 systemd 服务
|
|
83
|
-
|
|
84
|
-
|
|
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"
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
# 清理旧版 user 级别服务
|
|
88
|
+
if systemctl --user is-active way-backup.timer &>/dev/null || [ -f "$USER_TIMER" ]; then
|
|
87
89
|
echo ""
|
|
88
|
-
echo "检测到旧版 systemd timer
|
|
89
|
-
|
|
90
|
-
# 停止并禁用旧 timer
|
|
90
|
+
echo "检测到旧版 user 级 systemd timer,正在清理..."
|
|
91
91
|
systemctl --user stop way-backup.timer 2>/dev/null || true
|
|
92
92
|
systemctl --user disable way-backup.timer 2>/dev/null || true
|
|
93
|
-
rm -f "$
|
|
94
|
-
|
|
95
|
-
# 停止旧 service(如果在运行)
|
|
93
|
+
rm -f "$USER_TIMER"
|
|
96
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
|
|
97
99
|
|
|
98
|
-
|
|
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
|
|
100
|
+
if systemctl --user is-active way-backup.service &>/dev/null || [ -f "$USER_SERVICE" ]; then
|
|
103
101
|
echo ""
|
|
104
|
-
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
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
# 安装 system 级别服务
|
|
110
|
+
echo ""
|
|
111
|
+
echo "安装 system 级 systemd service..."
|
|
112
|
+
way systemd install
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
else
|
|
112
|
-
echo ""
|
|
113
|
-
echo "未检测到 systemd 服务,如需安装: way systemd install"
|
|
114
|
-
fi
|
|
114
|
+
echo "✓ systemd 服务已升级为 system 级别"
|
|
115
115
|
|
|
116
116
|
echo ""
|
|
117
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.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "将备份作为持续运营的项目,而非一次性任务。基于 restic 的策略封装。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
+
"vendor",
|
|
18
19
|
"repositories.yaml.example",
|
|
19
20
|
"rules.yaml.example",
|
|
20
21
|
"migrate-to-v0.5.sh"
|
|
@@ -38,9 +39,6 @@
|
|
|
38
39
|
"engines": {
|
|
39
40
|
"node": ">=18.0.0"
|
|
40
41
|
},
|
|
41
|
-
"peerDependencies": {
|
|
42
|
-
"restic": "*"
|
|
43
|
-
},
|
|
44
42
|
"dependencies": {
|
|
45
43
|
"commander": "^14.0.3",
|
|
46
44
|
"execa": "^9.6.1",
|
package/rules.yaml.example
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
# 全局默认配置
|
|
6
6
|
defaults:
|
|
7
|
-
schedule
|
|
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 * * *"
|
|
8
13
|
retention:
|
|
9
14
|
keep_daily: 7
|
|
10
15
|
keep_weekly: 4
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014, Alexander Neumann <alexander@bumpern.de>
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
7
|
+
are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
17
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
18
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
20
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
21
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
22
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
23
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
24
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
25
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
restic 0.16.4 compiled with go1.22.2 on linux/amd64
|
|
Binary file
|