@shellus/way 0.5.2 → 0.6.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 CHANGED
@@ -68,7 +68,7 @@
68
68
  ## 依赖
69
69
 
70
70
  - Node.js >= 18
71
- - Linux x64 平台内置 [restic](https://restic.net/) 0.16.4,其他平台需自行安装 restic
71
+ - Linux x64 平台内置 [restic](https://restic.net/) 0.18.1,其他平台需自行安装 restic
72
72
 
73
73
  `way` 查找 restic 的顺序:
74
74
 
@@ -85,7 +85,7 @@ npm install -g @shellus/way
85
85
  Linux x64 用户无需额外安装 restic。如需使用自定义 restic,可设置:
86
86
 
87
87
  ```bash
88
- WAY_RESTIC_BIN=/usr/local/bin/restic way snapshots
88
+ WAY_RESTIC_BIN=/usr/local/bin/restic way restic snapshots
89
89
  ```
90
90
 
91
91
  ## 快速开始
@@ -124,18 +124,23 @@ way systemd status
124
124
  ### 4. 手动执行备份
125
125
 
126
126
  ```bash
127
- way run # 备份所有项目
128
- way run data # 只备份 data 项目
129
- way snapshots # 查看快照
127
+ way backup # 备份所有项目
128
+ way backup data # 只备份 data 项目
129
+ way restore data --target /tmp/restore --dry-run # 按规则预演恢复
130
+ way restic snapshots # 查看快照
130
131
  ```
131
132
 
132
133
  ## 命令参考
133
134
 
134
135
  ```bash
135
136
  # 备份命令
136
- way run # 执行所有项目备份
137
- way run data # 只备份 data 项目
138
- way run data config # 备份多个项目
137
+ way backup # 执行所有项目备份
138
+ way backup data # 只备份 data 项目
139
+ way backup data config # 备份多个项目
140
+ way backup --dry-run # 模拟备份(不实际写入)
141
+ way restore data --target /tmp/restore # 按规则恢复 data 项目
142
+ way restore data --host old-host --target /tmp/restore --dry-run # 从指定 host 模拟恢复
143
+ way restore data --target /tmp/restore --delete # 删除目标中快照不存在的文件
139
144
  way gc # 按 retention 策略清理旧快照
140
145
  way gc --dry-run # 模拟清理(不实际删除)
141
146
 
@@ -148,14 +153,14 @@ way systemd show # 显示 systemd 配置
148
153
  way systemd status # 查看服务状态
149
154
  way systemd uninstall # 卸载服务
150
155
 
151
- # 透传 restic(way 只设置环境变量)
152
- way snapshots # → restic snapshots
153
- way restore abc123 # → restic restore abc123
154
- way check # → restic check
155
- way stats # → restic stats
156
+ # 显式透传 restic(way 只设置环境变量)
157
+ way restic snapshots # → restic snapshots
158
+ way restic check # → restic check
159
+ way restic stats # → restic stats
160
+ way restic restore abc123 --target /tmp/restore # → restic restore abc123 --target /tmp/restore
156
161
 
157
162
  # 指定 repository(默认用 repositories.yaml 中的 default)
158
- way --remote=oss snapshots
163
+ way --remote=oss restic snapshots
159
164
  ```
160
165
 
161
166
  ## 生命周期
@@ -186,7 +191,7 @@ cp ~/.way/rules.yaml.example ~/.way/rules.yaml
186
191
  也可以通过 `WAY_DIR` 环境变量指定其他目录:
187
192
 
188
193
  ```bash
189
- WAY_DIR=/path/to/config way snapshots
194
+ WAY_DIR=/path/to/config way restic snapshots
190
195
  ```
191
196
 
192
197
  ### rules.yaml
@@ -287,42 +292,68 @@ gpg -c way-config-$(date +%Y%m%d).tar.gz
287
292
 
288
293
  ## 数据恢复
289
294
 
290
- way 透传所有 restic 命令,恢复数据时只需通过 way 调用 restic 的恢复相关命令。way 负责设置仓库连接和认证环境变量,无需手动配置。
295
+ way 提供两种恢复方式:
296
+
297
+ 1. **按规则恢复**:使用 `way restore`,根据 `rules.yaml` 中的项目路径和 `way:<project>` 标签恢复。
298
+ 2. **原始 restic 恢复**:使用 `way restic restore`,显式透传 restic 参数。
291
299
 
292
300
  ### 1. 查看快照列表
293
301
 
294
302
  ```bash
295
- way snapshots # 列出所有快照
296
- way snapshots --tag=way:home # 只看 home 项目的快照
297
- way snapshots --tag=way:data # 只看 data 项目的快照
303
+ way restic snapshots # 列出所有快照
304
+ way restic snapshots --tag=way:home # 只看 home 项目的快照
305
+ way restic snapshots --tag=way:data # 只看 data 项目的快照
298
306
  ```
299
307
 
300
308
  ### 2. 浏览快照内容
301
309
 
302
310
  ```bash
303
- way ls <snapshot-id> # 列出快照中的所有文件
304
- way ls <snapshot-id> /root/.ssh # 列出快照中指定目录的文件
305
- way find --snapshot <id> <filename> # 在快照中搜索文件
311
+ way restic ls <snapshot-id> # 列出快照中的所有文件
312
+ way restic ls <snapshot-id> /root/.ssh # 列出快照中指定目录的文件
313
+ way restic find --snapshot <id> <filename> # 在快照中搜索文件
314
+ ```
315
+
316
+ ### 3. 按规则恢复项目
317
+
318
+ ```bash
319
+ # 恢复 data 项目的最新快照到指定目录
320
+ way restore data --target /tmp/restore
321
+
322
+ # 恢复所有项目
323
+ way restore --target /tmp/restore
324
+
325
+ # 指定快照 ID
326
+ way restore data --snapshot abc123 --target /tmp/restore
327
+
328
+ # 预演恢复,不实际写入
329
+ way restore data --host old-host --target /tmp/restore --dry-run
330
+
331
+ # 让目标目录与快照一致,先 dry-run 核对删除清单
332
+ way restore data --host old-host --target /tmp/restore --delete --dry-run
333
+ way restore data --host old-host --target /tmp/restore --delete
306
334
  ```
307
335
 
308
- ### 3. 恢复文件
336
+ `way restore` 会为项目自动添加 `--tag=way:<project>` 和项目 `paths` 对应的 `--include` 参数;传入 `--host` 时会只恢复指定 host 的快照。
337
+
338
+ ### 4. 原始 restic 恢复
309
339
 
310
340
  ```bash
311
- # 恢复整个快照到指定目录
312
- way restore <snapshot-id> --target /tmp/restore
341
+ # 显式透传 restic restore
342
+ way restic restore <snapshot-id> --target /tmp/restore
313
343
 
314
344
  # 只恢复特定路径
315
- way restore <snapshot-id> --target /tmp/restore --include /root/.ssh
345
+ way restic restore <snapshot-id> --target /tmp/restore --include /root/.ssh
316
346
 
317
347
  # 恢复单个文件到标准输出(适合快速查看)
318
- way dump <snapshot-id> /root/.gitconfig
348
+ way restic dump <snapshot-id> /root/.gitconfig
319
349
  ```
320
350
 
321
- ### 4. 从其他仓库恢复
351
+ ### 5. 从其他仓库恢复
322
352
 
323
353
  ```bash
324
- way --remote=oss snapshots
325
- way --remote=oss restore <snapshot-id> --target /tmp/restore
354
+ way --remote=oss restic snapshots
355
+ way --remote=oss restore data --target /tmp/restore
356
+ way --remote=oss restic restore <snapshot-id> --target /tmp/restore
326
357
  ```
327
358
 
328
359
  ## 开发
package/dist/cli.js CHANGED
@@ -80,6 +80,20 @@ function buildBackupArgs(name, project, globalExcludes) {
80
80
  args.push(...project.paths);
81
81
  return args;
82
82
  }
83
+ function buildRestoreArgs(name, project, options) {
84
+ const args = [
85
+ "restore",
86
+ options.snapshot || "latest",
87
+ `--tag=way:${name}`
88
+ ];
89
+ if (options.host) args.push(`--host=${options.host}`);
90
+ args.push(`--target=${options.target}`);
91
+ for (const path4 of project.paths) args.push(`--include=${path4}`);
92
+ if (options.dryRun) args.push("--dry-run");
93
+ if (options.delete) args.push("--delete");
94
+ if (options.verbose) args.push("--verbose=2");
95
+ return args;
96
+ }
83
97
  function buildS3Options(repo) {
84
98
  const options = [];
85
99
  if (repo.options?.bucket_lookup) {
@@ -100,8 +114,8 @@ async function execRestic(args, env, s3Options = []) {
100
114
  }
101
115
  }
102
116
 
103
- // src/commands/run.ts
104
- async function run(options) {
117
+ // src/commands/backup.ts
118
+ async function backup(options) {
105
119
  const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
106
120
  const config = loadConfig(wayDir, options.remote);
107
121
  const env = buildResticEnv(config.repository);
@@ -157,6 +171,48 @@ async function notifyUptimeKuma(result, pushUrl) {
157
171
  }
158
172
  }
159
173
 
174
+ // src/commands/restore.ts
175
+ async function restore(options) {
176
+ if (!options.target) throw new Error("--target is required");
177
+ const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
178
+ const config = loadConfig(wayDir, options.remote);
179
+ const env = buildResticEnv(config.repository);
180
+ const s3Options = buildS3Options(config.repository);
181
+ const projects = options.projects && options.projects.length > 0 ? options.projects : Object.keys(config.rules.projects);
182
+ const succeeded = [];
183
+ const failed = [];
184
+ const startTime = Date.now();
185
+ for (const projectName of projects) {
186
+ const project = config.rules.projects[projectName];
187
+ if (!project) {
188
+ console.error(`Project not found: ${projectName}`);
189
+ failed.push(projectName);
190
+ continue;
191
+ }
192
+ console.log(`=== Restoring: ${projectName} ===`);
193
+ try {
194
+ const args = buildRestoreArgs(projectName, project, {
195
+ target: options.target,
196
+ snapshot: options.snapshot,
197
+ host: options.host,
198
+ dryRun: options.dryRun,
199
+ delete: options.delete,
200
+ verbose: options.verbose
201
+ });
202
+ await execRestic(args, env, s3Options);
203
+ succeeded.push(projectName);
204
+ } catch (error) {
205
+ console.error(`Failed to restore ${projectName}:`, error);
206
+ failed.push(projectName);
207
+ }
208
+ }
209
+ const duration = Date.now() - startTime;
210
+ console.log("\n=== Summary ===");
211
+ if (succeeded.length > 0) console.log("Succeeded:", succeeded.join(", "));
212
+ if (failed.length > 0) console.log("Failed:", failed.join(", "));
213
+ return { succeeded, failed, duration };
214
+ }
215
+
160
216
  // src/commands/gc.ts
161
217
  async function gc(options) {
162
218
  const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
@@ -267,7 +323,7 @@ async function daemon(options) {
267
323
  cron.schedule(schedule, () => {
268
324
  executeTask(async () => {
269
325
  console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Running backup: ${name}`);
270
- await run({ remote: options.remote, projects: [name] });
326
+ await backup({ remote: options.remote, projects: [name] });
271
327
  });
272
328
  });
273
329
  console.log(`Scheduled backup for ${name}: ${schedule}`);
@@ -303,23 +359,41 @@ async function daemon(options) {
303
359
 
304
360
  // src/cli.ts
305
361
  var program = new Command();
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", `
362
+ program.name("way").version("0.6.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", `
307
363
  \u793A\u4F8B:
308
- $ way run \u6267\u884C\u6240\u6709\u9879\u76EE\u5907\u4EFD
309
- $ way run data \u53EA\u5907\u4EFD data \u9879\u76EE
310
- $ way run --dry-run \u6A21\u62DF\u5907\u4EFD\uFF08\u4E0D\u5B9E\u9645\u5199\u5165\uFF09
364
+ $ way backup \u6267\u884C\u6240\u6709\u9879\u76EE\u5907\u4EFD
365
+ $ way backup data \u53EA\u5907\u4EFD data \u9879\u76EE
366
+ $ way backup --dry-run \u6A21\u62DF\u5907\u4EFD\uFF08\u4E0D\u5B9E\u9645\u5199\u5165\uFF09
367
+ $ way restore data --target /tmp/restore --dry-run
311
368
  $ way gc \u6E05\u7406\u65E7\u5FEB\u7167
312
369
  $ way systemd install \u5B89\u88C5\u5B9A\u65F6\u4EFB\u52A1
313
- $ way snapshots \u67E5\u770B\u5FEB\u7167\u5217\u8868
314
- $ way --remote=s3 snapshots \u4F7F\u7528 s3 \u4ED3\u5E93
370
+ $ way restic snapshots \u67E5\u770B\u5FEB\u7167\u5217\u8868
371
+ $ way restic restore abc123 --target /tmp/restore
372
+ $ way --remote=s3 restic snapshots \u4F7F\u7528 s3 \u4ED3\u5E93
315
373
 
316
374
  \u6587\u6863: https://github.com/shellus/way
317
375
  `);
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) {
376
+ function collectBackupArgs(command) {
377
+ return command.args.filter((a) => a.startsWith("-") && !["--dry-run"].includes(a));
378
+ }
379
+ program.command("backup [projects...]").description("\u6309 rules.yaml \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
380
  const remote = this.parent.opts().remote;
320
381
  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 });
382
+ const extraArgs = collectBackupArgs(this);
383
+ await backup({ remote, projects: projects.filter((p) => !p.startsWith("-")), extraArgs, dryRun });
384
+ });
385
+ program.command("restore [projects...]").description("\u6309 rules.yaml \u6062\u590D\u9879\u76EE").requiredOption("--target <dir>", "\u6062\u590D\u76EE\u6807\u76EE\u5F55").option("--snapshot <snapshot>", "\u5FEB\u7167 ID \u6216 latest", "latest").option("--host <host>", "\u53EA\u6062\u590D\u6307\u5B9A host \u7684\u5FEB\u7167").option("--dry-run", "\u6A21\u62DF\u6062\u590D\uFF08\u4E0D\u5B9E\u9645\u5199\u5165\uFF09").option("--delete", "\u5220\u9664\u76EE\u6807\u4E2D\u5FEB\u7167\u4E0D\u5B58\u5728\u7684\u6587\u4EF6").option("-v, --verbose", "\u663E\u793A\u8BE6\u7EC6\u6062\u590D\u8BA1\u5212\uFF08\u4F20\u9012 --verbose=2 \u7ED9 restic\uFF09").action(async function(projects, cmdOptions) {
386
+ const remote = this.parent.opts().remote;
387
+ await restore({
388
+ remote,
389
+ projects,
390
+ target: cmdOptions.target,
391
+ snapshot: cmdOptions.snapshot,
392
+ host: cmdOptions.host,
393
+ dryRun: cmdOptions.dryRun,
394
+ delete: cmdOptions.delete,
395
+ verbose: cmdOptions.verbose
396
+ });
323
397
  });
324
398
  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) {
325
399
  const remote = this.parent.opts().remote;
@@ -339,13 +413,16 @@ program.command("env").description("\u663E\u793A\u73AF\u5883\u53D8\u91CF").actio
339
413
  console.log(`${key}=${value}`);
340
414
  }
341
415
  });
342
- program.command("*", { isDefault: true }).description("\u900F\u4F20\u7ED9 restic").allowUnknownOption().allowExcessArguments().action(async function(name) {
416
+ program.command("restic [args...]").description("\u663E\u5F0F\u900F\u4F20\u7ED9 restic").allowUnknownOption().allowExcessArguments().action(async function(args) {
343
417
  const remote = this.parent.opts().remote;
344
418
  const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
345
419
  const config = loadConfig(wayDir, remote);
346
420
  const env = buildResticEnv(config.repository);
347
421
  const s3Options = buildS3Options(config.repository);
348
- const args = this.parent.args;
349
422
  await execRestic(args, env, s3Options);
350
423
  });
424
+ if (process.argv.length <= 2) {
425
+ program.outputHelp();
426
+ process.exit(0);
427
+ }
351
428
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shellus/way",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "将备份作为持续运营的项目,而非一次性任务。基于 restic 的策略封装。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- restic 0.16.4 compiled with go1.22.2 on linux/amd64
1
+ restic 0.18.1 compiled with go1.25.1 on linux/amd64
Binary file