@shellus/way 0.3.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.
@@ -0,0 +1,44 @@
1
+ # 本机环境配置
2
+
3
+ ## 存储后端
4
+
5
+ 腾讯云 COS(成都区域):
6
+ - Endpoint: `cos.ap-chengdu.myqcloud.com`
7
+ - Bucket: `xa-backup-1251087920`
8
+ - 需要 `s3.bucket-lookup=dns` 选项
9
+
10
+ ## 备份项目
11
+
12
+ | 项目 | 路径 | 说明 |
13
+ |------|------|------|
14
+ | data | /data | Docker 服务、项目源码、开发工具配置 |
15
+ | home | /root/.ssh, /root/.claude 等 | SSH 密钥、Claude 配置、Shell 配置 |
16
+ | system | /etc/fstab | 系统挂载配置 |
17
+
18
+ ## 排除规则备忘
19
+
20
+ playwright-mcp 浏览器缓存目录名是 `browser-profile`(不是 `chrome-profile`)。
21
+
22
+ ## 定时任务
23
+
24
+ ```
25
+ 1 12 * * * way run # 12:01 午休后
26
+ 30 15 * * * way run # 15:30 下午
27
+ 30 18 * * * way run # 18:30 下班前
28
+ 0 4 * * 0 way gc # 每周日凌晨清理
29
+ ```
30
+
31
+ 安装:`way cron install`
32
+
33
+ ## 验证命令
34
+
35
+ ```bash
36
+ # 查看快照
37
+ way snapshots
38
+
39
+ # 检查排除规则
40
+ way ls <snapshot-id> | grep -E 'node_modules|Cache'
41
+
42
+ # 测试备份
43
+ way run --dry-run
44
+ ```
package/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # way - 策略备份
2
+
3
+ 将备份作为持续运营的项目,而非一次性任务。基于 restic 的策略封装。
4
+
5
+ 推荐在所有地方使用策略备份:开发环境、个人电脑、生产服务器。
6
+
7
+ ## 设计原理
8
+
9
+ ### 1. 备份目的
10
+
11
+ - 意外修改或删除时,从备份提取
12
+ - 硬盘损毁、机房失联时,从备份重建
13
+ - 需要追溯历史状态时,查阅备份
14
+
15
+ ### 2. 不备份可重建内容
16
+
17
+ 可重建的内容不备份:docker 镜像、node_modules、缓存、构建产物等。判断标准不是"是否有价值",而是"能否从其他来源恢复"。
18
+
19
+ ### 3. 备份是项目
20
+
21
+ 备份保护所有项目,其重要性是所有项目的总和。应作为最重要的项目持续运营。
22
+
23
+ ### 4. 频率匹配变化
24
+
25
+ 备份频率应匹配数据变化频率。高频变化时段多备份,低频时段少备份。
26
+
27
+ ### 5. 可验证原则
28
+
29
+ 未经验证的备份等于没有备份。定期验证备份可恢复,失败时必须有告警。
30
+
31
+ ### 6. 定期清理
32
+
33
+ 定期清理旧快照,防止存储膨胀。
34
+
35
+ ### 7. 同步不等于备份
36
+
37
+ 同步工具(如 Syncthing)会将误操作实时传播到所有节点。真正的备份必须具备版本历史和回滚能力。
38
+
39
+ ### 8. 备份隔离原则
40
+
41
+ 假设本机被入侵,攻击者不应能通过本机凭证定位或删除备份数据。
42
+
43
+ ### 9. 多层冗余原则
44
+
45
+ 单一备份链路存在单点故障风险。应在多个维度建立冗余:
46
+ - 时间周期:高频 + 每日 + 每周
47
+ - 存储介质:云存储 + 本地硬盘 + 异地服务器
48
+
49
+ ### 10. 恢复演练原则
50
+
51
+ 定期执行实际恢复演练,确认备份可解密、可读取,恢复时间在可接受范围内。
52
+
53
+ ---
54
+
55
+ ## 依赖
56
+
57
+ - [restic](https://restic.net/) - 备份引擎
58
+ - Node.js >= 18(npm 安装方式)或 yq(脚本安装方式)
59
+
60
+ npm 安装方式会自动处理依赖,脚本安装方式的 install.sh 会自动安装 restic 和 yq。
61
+
62
+ ## 快速开始
63
+
64
+ ### 方式 1: npm 安装(推荐)
65
+
66
+ ```bash
67
+ npm install -g @shellus/way
68
+
69
+ # 初始化配置
70
+ mkdir -p ~/.way
71
+ cp $(npm root -g)/@shellus/way/repositories.yaml.example ~/.way/repositories.yaml
72
+ cp $(npm root -g)/@shellus/way/rules.yaml.example ~/.way/rules.yaml
73
+ ```
74
+
75
+ ### 方式 2: 脚本安装
76
+
77
+ ```bash
78
+ # 安装(way + restic + yq)
79
+ curl -fsSL https://github.com/shellus/way/releases/latest/download/install.sh | bash
80
+ ```
81
+
82
+ ### 使用
83
+
84
+ ```bash
85
+
86
+ # way 自己的命令(不与 restic 冲突)
87
+ way run # 执行备份(读取 rules.yaml 的项目和排除规则)
88
+ way run data # 只备份 data 项目
89
+ way gc # 按 retention 策略清理旧快照
90
+ way cron install # 安装定时任务
91
+ way cron show # 显示将要安装的 crontab
92
+
93
+ # 透传 restic(way 只设置环境变量)
94
+ way snapshots # → restic snapshots
95
+ way restore abc123 # → restic restore abc123
96
+ way check # → restic check
97
+ way stats # → restic stats
98
+ way prune # → restic prune(直接透传)
99
+
100
+ # 指定 repository(默认用 repositories.yaml 中的 default)
101
+ way --remote=oss snapshots
102
+ ```
103
+
104
+ ---
105
+
106
+ ## 配置说明
107
+
108
+ 配置文件默认存放在 `~/.way/`,安装后复制示例文件并填入实际值:
109
+
110
+ ```bash
111
+ cp ~/.way/repositories.yaml.example ~/.way/repositories.yaml
112
+ cp ~/.way/rules.yaml.example ~/.way/rules.yaml
113
+ ```
114
+
115
+ 也可以通过 `WAY_DIR` 环境变量指定其他目录:
116
+
117
+ ```bash
118
+ WAY_DIR=/path/to/config way snapshots
119
+ ```
120
+
121
+ ### rules.yaml
122
+
123
+ 备份规则配置,参考 [`rules.yaml`](rules.yaml):
124
+
125
+ - **projects**: 备份项目、路径、专属排除规则
126
+ - **schedule**: 备份时间、清理任务的 cron 表达式
127
+ - **retention**: 快照保留策略
128
+ - **global_excludes**: 全局排除规则
129
+
130
+ #### 排除规则通配符语法
131
+
132
+ restic 使用 Go 的 filepath.Match 语法:
133
+
134
+ - `*` 匹配任意字符,但**不跨越目录分隔符**
135
+ - `**` 匹配任意子目录
136
+
137
+ 示例:
138
+ - `*Cache*` 只匹配当前目录下含 Cache 的文件/目录
139
+ - `**/*Cache*` 匹配任意深度子目录下含 Cache 的文件/目录
140
+
141
+ ### repositories.yaml
142
+
143
+ 备份目的地配置,参考 [`repositories.yaml`](repositories.yaml)。
144
+
145
+ ### .env
146
+
147
+ 敏感凭证通过环境变量注入,在 `.env` 中定义:
148
+
149
+ ```bash
150
+ RESTIC_PASSWORD=your-restic-password
151
+ AWS_ACCESS_KEY_ID=your-access-key
152
+ AWS_SECRET_ACCESS_KEY=your-secret-key
153
+ ```
154
+
155
+ 在 `repositories.yaml` 中使用 `${VAR}` 语法引用:
156
+
157
+ ```yaml
158
+ credentials:
159
+ password: ${RESTIC_PASSWORD}
160
+ access_key_id: ${AWS_ACCESS_KEY_ID}
161
+ ```
162
+
163
+ ## 凭证离线备份
164
+
165
+ `repositories.yaml` 和 `.env` 包含 restic 仓库密码和云存储凭证。这两个文件被 `.gitignore` 排除,不会进入版本控制,也不会被 way 自身备份。
166
+
167
+ **如果本机不可访问且遗忘了这些凭证,所有备份数据将永久无法恢复。**
168
+
169
+ 必须在本机之外单独保存一份:
170
+
171
+ - 密码管理器(推荐)
172
+ - 加密笔记
173
+ - 离线存储(U 盘、纸质打印)
174
+
175
+ 需要保存的信息:
176
+
177
+ | 项目 | 来源 |
178
+ |------|------|
179
+ | restic 仓库密码 | `.env` 中的 `RESTIC_PASSWORD` |
180
+ | S3 访问凭证 | `.env` 中的 `AWS_ACCESS_KEY_ID`、`AWS_SECRET_ACCESS_KEY` |
181
+ | 仓库地址 | `repositories.yaml` 中的 `url` |
182
+
183
+ ## 数据恢复
184
+
185
+ way 透传所有 restic 命令,恢复数据时只需通过 way 调用 restic 的恢复相关命令。way 负责设置仓库连接和认证环境变量,无需手动配置。
186
+
187
+ ### 1. 查看快照列表
188
+
189
+ ```bash
190
+ way snapshots # 列出所有快照
191
+ way snapshots --tag=way:home # 只看 home 项目的快照
192
+ way snapshots --tag=way:data # 只看 data 项目的快照
193
+ ```
194
+
195
+ ### 2. 浏览快照内容
196
+
197
+ ```bash
198
+ way ls <snapshot-id> # 列出快照中的所有文件
199
+ way ls <snapshot-id> /root/.ssh # 列出快照中指定目录的文件
200
+ way find --snapshot <id> <filename> # 在快照中搜索文件
201
+ ```
202
+
203
+ ### 3. 恢复文件
204
+
205
+ ```bash
206
+ # 恢复整个快照到指定目录
207
+ way restore <snapshot-id> --target /tmp/restore
208
+
209
+ # 只恢复特定路径
210
+ way restore <snapshot-id> --target /tmp/restore --include /root/.ssh
211
+
212
+ # 恢复单个文件到标准输出(适合快速查看)
213
+ way dump <snapshot-id> /root/.gitconfig
214
+ ```
215
+
216
+ ### 4. 从其他仓库恢复
217
+
218
+ ```bash
219
+ way --remote=oss snapshots
220
+ way --remote=oss restore <snapshot-id> --target /tmp/restore
221
+ ```
222
+
223
+ ## 开发
224
+
225
+ 参见 [CLAUDE.md](CLAUDE.md)。
package/dist/cli.js ADDED
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/core/config.ts
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import yaml from "js-yaml";
10
+ import dotenv from "dotenv";
11
+
12
+ // src/core/env.ts
13
+ function expandEnv(value) {
14
+ if (!value) return "";
15
+ const match = value.match(/^\$\{(.+)\}$/);
16
+ if (match) {
17
+ return process.env[match[1]] || "";
18
+ }
19
+ return value;
20
+ }
21
+
22
+ // src/core/config.ts
23
+ function loadConfig(wayDir, remoteName) {
24
+ const envPath = path.join(wayDir, ".env");
25
+ if (fs.existsSync(envPath)) {
26
+ dotenv.config({ path: envPath });
27
+ }
28
+ const repoFile = path.join(wayDir, "repositories.yaml");
29
+ const repoConfig = yaml.load(fs.readFileSync(repoFile, "utf8"));
30
+ const repoName = remoteName === "default" ? repoConfig.default : remoteName;
31
+ const repository = repoConfig.repositories[repoName];
32
+ if (!repository) throw new Error(`Repository not found: ${repoName}`);
33
+ if (repository.credentials.password) {
34
+ repository.credentials.password = expandEnv(repository.credentials.password);
35
+ }
36
+ if (repository.credentials.access_key_id) {
37
+ repository.credentials.access_key_id = expandEnv(repository.credentials.access_key_id);
38
+ }
39
+ if (repository.credentials.secret_access_key) {
40
+ repository.credentials.secret_access_key = expandEnv(repository.credentials.secret_access_key);
41
+ }
42
+ const rulesFile = path.join(wayDir, "rules.yaml");
43
+ const rules = yaml.load(fs.readFileSync(rulesFile, "utf8"));
44
+ if (rules.uptime_kuma?.push_url) {
45
+ rules.uptime_kuma.push_url = expandEnv(rules.uptime_kuma.push_url);
46
+ }
47
+ return { repository, rules };
48
+ }
49
+
50
+ // src/core/restic.ts
51
+ import { execa } from "execa";
52
+ function buildResticEnv(repo) {
53
+ const env = {};
54
+ switch (repo.type) {
55
+ case "s3":
56
+ env.RESTIC_REPOSITORY = `s3:https://${repo.endpoint}/${repo.bucket}`;
57
+ break;
58
+ case "local":
59
+ env.RESTIC_REPOSITORY = repo.path;
60
+ break;
61
+ case "sftp":
62
+ env.RESTIC_REPOSITORY = `sftp:${repo.host}:${repo.path}`;
63
+ break;
64
+ }
65
+ if (repo.credentials.password) env.RESTIC_PASSWORD = repo.credentials.password;
66
+ if (repo.credentials.access_key_id) env.AWS_ACCESS_KEY_ID = repo.credentials.access_key_id;
67
+ if (repo.credentials.secret_access_key) env.AWS_SECRET_ACCESS_KEY = repo.credentials.secret_access_key;
68
+ return env;
69
+ }
70
+ function buildBackupArgs(name, project, globalExcludes) {
71
+ const args = ["backup", `--tag=way:${name}`];
72
+ const allExcludes = [...globalExcludes, ...project.excludes || []];
73
+ for (const exclude of allExcludes) args.push(`--exclude=${exclude}`);
74
+ args.push(...project.paths);
75
+ return args;
76
+ }
77
+ function buildS3Options(repo) {
78
+ const options = [];
79
+ if (repo.options?.bucket_lookup) {
80
+ options.push("-o", `s3.bucket-lookup=${repo.options.bucket_lookup}`);
81
+ }
82
+ return options;
83
+ }
84
+ async function execRestic(args, env, s3Options = []) {
85
+ await execa("restic", [...s3Options, ...args], { env: { ...process.env, ...env }, stdio: "inherit" });
86
+ }
87
+
88
+ // src/commands/run.ts
89
+ async function run(options) {
90
+ const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
91
+ const config = loadConfig(wayDir, options.remote);
92
+ const env = buildResticEnv(config.repository);
93
+ const s3Options = buildS3Options(config.repository);
94
+ const globalExcludes = config.rules.global_excludes || [];
95
+ const projects = options.project ? [options.project] : Object.keys(config.rules.projects);
96
+ const succeeded = [];
97
+ const failed = [];
98
+ const startTime = Date.now();
99
+ for (const projectName of projects) {
100
+ const project = config.rules.projects[projectName];
101
+ if (!project) {
102
+ console.error(`Project not found: ${projectName}`);
103
+ failed.push(projectName);
104
+ continue;
105
+ }
106
+ console.log(`=== Backing up: ${projectName} ===`);
107
+ try {
108
+ const args = buildBackupArgs(projectName, project, globalExcludes);
109
+ if (options.extraArgs) args.push(...options.extraArgs);
110
+ await execRestic(args, env, s3Options);
111
+ succeeded.push(projectName);
112
+ } catch (error) {
113
+ console.error(`Failed to backup ${projectName}:`, error);
114
+ failed.push(projectName);
115
+ }
116
+ }
117
+ const duration = Date.now() - startTime;
118
+ console.log("\n=== Summary ===");
119
+ if (succeeded.length > 0) console.log("Succeeded:", succeeded.join(", "));
120
+ if (failed.length > 0) console.log("Failed:", failed.join(", "));
121
+ if (config.rules.uptime_kuma?.push_url) {
122
+ await notifyUptimeKuma({ succeeded, failed, duration }, config.rules.uptime_kuma.push_url);
123
+ }
124
+ return { succeeded, failed, duration };
125
+ }
126
+ async function notifyUptimeKuma(result, pushUrl) {
127
+ const status = result.failed.length > 0 ? "down" : "up";
128
+ const msg = `Succeeded: ${result.succeeded.length}, Failed: ${result.failed.length}`;
129
+ const url = `${pushUrl}?status=${status}&msg=${encodeURIComponent(msg)}&ping=${result.duration}`;
130
+ try {
131
+ const response = await fetch(url);
132
+ if (response.ok) {
133
+ console.log(`Uptime Kuma notified: status=${status}, ping=${result.duration}ms`);
134
+ } else {
135
+ console.error("Uptime Kuma notification failed");
136
+ }
137
+ } catch (error) {
138
+ console.error("Uptime Kuma notification failed:", error);
139
+ }
140
+ }
141
+
142
+ // src/commands/gc.ts
143
+ async function gc(options) {
144
+ const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
145
+ const config = loadConfig(wayDir, options.remote);
146
+ const keepDaily = config.rules.retention?.keep_daily || 7;
147
+ const keepWeekly = config.rules.retention?.keep_weekly || 4;
148
+ const keepMonthly = config.rules.retention?.keep_monthly || 6;
149
+ console.log("=== Cleaning snapshots ===");
150
+ console.log(`Policy: daily=${keepDaily}, weekly=${keepWeekly}, monthly=${keepMonthly}`);
151
+ const env = buildResticEnv(config.repository);
152
+ const s3Options = buildS3Options(config.repository);
153
+ const args = [
154
+ "forget",
155
+ "--prune",
156
+ `--keep-daily=${keepDaily}`,
157
+ `--keep-weekly=${keepWeekly}`,
158
+ `--keep-monthly=${keepMonthly}`
159
+ ];
160
+ if (options.extraArgs) args.push(...options.extraArgs);
161
+ await execRestic(args, env, s3Options);
162
+ }
163
+
164
+ // src/commands/cron.ts
165
+ import { execSync } from "child_process";
166
+ import fs2 from "fs";
167
+ async function cron(options) {
168
+ const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
169
+ const config = loadConfig(wayDir, options.remote);
170
+ const markerStart = "# === way backup schedule ===";
171
+ const markerEnd = "# === way backup schedule end ===";
172
+ const generateCron = () => {
173
+ const lines = [markerStart];
174
+ const backupSchedules = config.rules.schedule?.backup || [];
175
+ for (const schedule of backupSchedules) {
176
+ lines.push(`${schedule} /usr/local/bin/way run`);
177
+ }
178
+ if (config.rules.schedule?.prune) {
179
+ lines.push(`${config.rules.schedule.prune} /usr/local/bin/way gc`);
180
+ }
181
+ if (config.rules.schedule?.check) {
182
+ lines.push(`${config.rules.schedule.check} /usr/local/bin/way check`);
183
+ }
184
+ lines.push(markerEnd);
185
+ return lines.join("\n");
186
+ };
187
+ if (options.action === "show") {
188
+ console.log(generateCron());
189
+ return;
190
+ }
191
+ if (options.action === "install") {
192
+ if (!fs2.existsSync("/usr/local/bin/way")) {
193
+ throw new Error("Error: /usr/local/bin/way not found");
194
+ }
195
+ let existing = "";
196
+ try {
197
+ existing = execSync("crontab -l 2>/dev/null", { encoding: "utf8" });
198
+ } catch {
199
+ }
200
+ const filtered = existing.split("\n").filter((line) => {
201
+ return !line.includes(markerStart) && !line.includes(markerEnd);
202
+ }).join("\n");
203
+ const newCrontab = filtered + "\n" + generateCron();
204
+ execSync("crontab -", { input: newCrontab });
205
+ console.log("Crontab installed successfully");
206
+ execSync('crontab -l | grep -A100 "' + markerStart + '" | head -20', { stdio: "inherit", shell: "/bin/bash" });
207
+ }
208
+ }
209
+
210
+ // src/cli.ts
211
+ var program = new Command();
212
+ program.name("way").version("0.3.0").description("\u7B56\u7565\u5907\u4EFD\u5DE5\u5177").option("--remote <name>", "\u6307\u5B9A\u4ED3\u5E93", "default");
213
+ program.command("run [project]").description("\u6267\u884C\u5907\u4EFD").action(async (project, options, command) => {
214
+ const remote = command.parent.opts().remote;
215
+ const extraArgs = command.args.slice(1);
216
+ await run({ remote, project, extraArgs });
217
+ });
218
+ program.command("gc").description("\u6E05\u7406\u65E7\u5FEB\u7167").allowUnknownOption().allowExcessArguments().action(async function() {
219
+ const remote = this.parent.opts().remote;
220
+ const extraArgs = this.parent.args.slice(1);
221
+ await gc({ remote, extraArgs });
222
+ });
223
+ program.command("cron <action>").description("\u7BA1\u7406\u5B9A\u65F6\u4EFB\u52A1 (show|install)").action(async (action, options, command) => {
224
+ const remote = command.parent.opts().remote;
225
+ await cron({ remote, action });
226
+ });
227
+ program.command("env").description("\u663E\u793A\u73AF\u5883\u53D8\u91CF").action(() => {
228
+ const env = Object.entries(process.env).sort(([a], [b]) => a.localeCompare(b));
229
+ for (const [key, value] of env) {
230
+ console.log(`${key}=${value}`);
231
+ }
232
+ });
233
+ program.command("*", { isDefault: true }).description("\u900F\u4F20\u7ED9 restic").allowUnknownOption().allowExcessArguments().action(async function(name) {
234
+ const remote = this.parent.opts().remote;
235
+ const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
236
+ const config = loadConfig(wayDir, remote);
237
+ const env = buildResticEnv(config.repository);
238
+ const s3Options = buildS3Options(config.repository);
239
+ const args = this.parent.args;
240
+ await execRestic(args, env, s3Options);
241
+ });
242
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@shellus/way",
3
+ "version": "0.3.0",
4
+ "description": "将备份作为持续运营的项目,而非一次性任务。基于 restic 的策略封装。",
5
+ "type": "module",
6
+ "bin": {
7
+ "way": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup src/cli.ts --format esm --clean",
11
+ "dev": "tsup src/cli.ts --format esm --watch",
12
+ "test": "vitest",
13
+ "test:run": "vitest run",
14
+ "prepublishOnly": "npm run test:run && npm run build"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "repositories.yaml.example",
19
+ "rules.yaml.example"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/shellus/way.git"
24
+ },
25
+ "keywords": ["backup", "restic", "snapshot", "cron"],
26
+ "author": "shellus",
27
+ "license": "MIT",
28
+ "bugs": {
29
+ "url": "https://github.com/shellus/way/issues"
30
+ },
31
+ "homepage": "https://github.com/shellus/way#readme",
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "peerDependencies": {
36
+ "restic": "*"
37
+ },
38
+ "dependencies": {
39
+ "commander": "^14.0.3",
40
+ "dotenv": "^17.3.1",
41
+ "execa": "^9.6.1",
42
+ "js-yaml": "^4.1.1"
43
+ },
44
+ "devDependencies": {
45
+ "@types/js-yaml": "^4.0.9",
46
+ "@types/node": "^25.5.0",
47
+ "tsup": "^8.5.1",
48
+ "typescript": "^5.9.3",
49
+ "vitest": "^4.1.0"
50
+ }
51
+ }
@@ -0,0 +1,30 @@
1
+ # 备份目的地配置 (restic repositories)
2
+ # 凭证支持直接填写或引用环境变量: ${ENV_VAR}
3
+ # 复制为 repositories.yaml 并填入实际值
4
+
5
+ default: local
6
+
7
+ repositories:
8
+ local:
9
+ type: local
10
+ path: /tmp/way-backup
11
+ credentials:
12
+ password: ${RESTIC_PASSWORD}
13
+
14
+ s3:
15
+ type: s3
16
+ endpoint: s3.example.com
17
+ bucket: my-backup-bucket
18
+ options:
19
+ bucket_lookup: dns
20
+ credentials:
21
+ password: ${RESTIC_PASSWORD}
22
+ access_key_id: ${AWS_ACCESS_KEY_ID}
23
+ secret_access_key: ${AWS_SECRET_ACCESS_KEY}
24
+
25
+ sftp:
26
+ type: sftp
27
+ host: backup.example.com
28
+ path: /backup/myhost
29
+ credentials:
30
+ password: ${RESTIC_PASSWORD}
@@ -0,0 +1,63 @@
1
+ # 备份规则配置
2
+ # 备份目的地见 repositories.yaml
3
+ # 复制为 rules.yaml 并根据实际情况修改
4
+
5
+ # Uptime Kuma 通知(备份完成后推送状态)
6
+ uptime_kuma:
7
+ push_url: "${UPTIME_KUMA_PUSH_URL}" # 支持环境变量
8
+
9
+ # 备份时间 (cron格式,留空则不执行)
10
+ schedule:
11
+ backup:
12
+ - "0 12 * * *" # 每天中午
13
+ - "0 18 * * *" # 每天傍晚
14
+ prune: "0 4 * * 0" # 每周日凌晨4点清理旧快照
15
+ check: "" # 完整性检查,留空不执行
16
+
17
+ # 快照保留策略
18
+ retention:
19
+ keep_daily: 7
20
+ keep_weekly: 4
21
+ keep_monthly: 6
22
+
23
+ projects:
24
+ data:
25
+ description: 数据目录
26
+ paths:
27
+ - /path/to/data
28
+ excludes:
29
+ - cache
30
+ - tmp
31
+
32
+ config:
33
+ description: 配置文件
34
+ paths:
35
+ - /home/user/.config
36
+ - /home/user/.ssh
37
+ excludes: []
38
+
39
+ # 全局排除规则 - 适用于所有项目
40
+ global_excludes:
41
+ # 包管理
42
+ - node_modules
43
+ - .venv
44
+ - venv
45
+ - __pycache__
46
+ - "*.pyc"
47
+ - vendor
48
+
49
+ # 缓存
50
+ - .cache
51
+
52
+ # 构建产物
53
+ - dist
54
+ - .next
55
+
56
+ # IDE配置
57
+ - .idea
58
+ - .vscode
59
+
60
+ # 临时文件
61
+ - "*.log"
62
+ - "*.tmp"
63
+ - "*.swp"