@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 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\u4E0D\u518D\u652F\u6301\uFF0C\u8BF7\u53C2\u8003 rules.yaml.example \u66F4\u65B0\u914D\u7F6E");
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("restic", [...s3Options, ...args], { env: { ...process.env, ...env }, stdio: "inherit" });
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. Please install restic first.");
64
- console.error("Visit: https://restic.net/");
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 (options.extraArgs) args.push(...options.extraArgs);
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 fs2 from "fs";
153
- import path2 from "path";
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=default.target
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 = `${process.env.HOME}/.config/systemd/user`;
178
- const servicePath = path2.join(systemdDir, "way-backup.service");
215
+ const systemdDir = "/etc/systemd/system";
216
+ const servicePath = path3.join(systemdDir, "way-backup.service");
179
217
  if (options.action === "install") {
180
- fs2.mkdirSync(systemdDir, { recursive: true });
181
- fs2.writeFileSync(servicePath, serviceContent);
182
- execSync("systemctl --user daemon-reload");
183
- execSync("systemctl --user enable way-backup.service");
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 --user --no-pager status way-backup.service", { stdio: "inherit" });
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 --user stop way-backup.service", { stdio: "ignore" });
227
+ execSync("systemctl stop way-backup.service", { stdio: "ignore" });
191
228
  } catch {
192
229
  }
193
230
  try {
194
- execSync("systemctl --user disable way-backup.service", { stdio: "ignore" });
231
+ execSync("systemctl disable way-backup.service", { stdio: "ignore" });
195
232
  } catch {
196
233
  }
197
- if (fs2.existsSync(servicePath)) fs2.unlinkSync(servicePath);
198
- execSync("systemctl --user daemon-reload");
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 --user --no-pager status way-backup.service", { stdio: "inherit" });
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.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", `
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, options, command) => {
282
- const remote = command.parent.opts().remote;
283
- await run({ remote, projects });
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;
@@ -80,38 +80,38 @@ fi
80
80
  echo "✓ 配置迁移完成"
81
81
 
82
82
  # 检测并升级 systemd 服务
83
- TIMER_FILE="$HOME/.config/systemd/user/way-backup.timer"
84
- SERVICE_FILE="$HOME/.config/systemd/user/way-backup.service"
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
- if systemctl --user is-active way-backup.timer &>/dev/null || [ -f "$TIMER_FILE" ]; then
87
+ # 清理旧版 user 级别服务
88
+ if systemctl --user is-active way-backup.timer &>/dev/null || [ -f "$USER_TIMER" ]; then
87
89
  echo ""
88
- echo "检测到旧版 systemd timer,正在升级为 daemon service..."
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 "$TIMER_FILE"
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
- # 重新安装新版 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
100
+ if systemctl --user is-active way-backup.service &>/dev/null || [ -f "$USER_SERVICE" ]; then
103
101
  echo ""
104
- echo "检测到 systemd service,正在重新安装..."
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
- way systemd uninstall
108
- way systemd install
109
+ # 安装 system 级别服务
110
+ echo ""
111
+ echo "安装 system 级 systemd service..."
112
+ way systemd install
109
113
 
110
- echo "✓ systemd 服务已更新"
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.0",
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",
@@ -4,7 +4,12 @@
4
4
 
5
5
  # 全局默认配置
6
6
  defaults:
7
- schedule: "0 */2 * * *" # 默认每 2 小时
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