create-ssnode-cli 0.2.5 → 0.2.7

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.
Files changed (87) hide show
  1. package/README.md +1 -1
  2. package/index.js +17 -21
  3. package/package.json +1 -1
  4. package/secrets.js +32 -34
  5. package/templates/backend/Dockerfile +18 -8
  6. package/templates/backend/config/config.base.yaml +130 -0
  7. package/templates/backend/config/config.develop.yaml +8 -75
  8. package/templates/backend/config/config.prod.yaml +12 -67
  9. package/templates/backend/docker-compose.yml +30 -22
  10. package/templates/backend/ops/docker/config.sh +30 -15
  11. package/templates/backend/ops/docker/install.sh +25 -34
  12. package/templates/backend/ops/docker/run-container.sh +100 -0
  13. package/templates/backend/ops/docker/start.sh +37 -8
  14. package/templates/backend/ops/docker/stop.sh +1 -1
  15. package/templates/backend/ops/ensure-encrypt.cjs +217 -0
  16. package/templates/backend/ops/install.sh +249 -148
  17. package/templates/backend/ops/lib-encrypt.sh +98 -0
  18. package/templates/backend/ops/lib.sh +22 -0
  19. package/templates/backend/ops/pm2/config.sh +11 -6
  20. package/templates/backend/ops/pm2/install.sh +27 -3
  21. package/templates/backend/ops/pm2/start.sh +1 -1
  22. package/templates/backend/ops/start.sh +7 -5
  23. package/templates/backend/ops/status.sh +7 -5
  24. package/templates/backend/ops/stop.sh +7 -5
  25. package/templates/backend/ops/systemd/config.sh +13 -6
  26. package/templates/backend/ops/systemd/install.sh +28 -1
  27. package/templates/backend/ops/systemd/start.sh +1 -1
  28. package/templates/backend/ops/versionswitch.sh +34 -122
  29. package/templates/backend/package-lock.json +7561 -0
  30. package/templates/backend/package.json +3 -1
  31. package/templates/backend/scripts/build.cjs +104 -10
  32. package/templates/backend/scripts/config-crypt.cjs +108 -0
  33. package/templates/backend/scripts/lib/ssh.cjs +72 -3
  34. package/templates/backend/scripts/publish.cjs +17 -9
  35. package/templates/backend/scripts/pulldocker.cjs +208 -0
  36. package/templates/backend/scripts/pushdocker.cjs +323 -0
  37. package/templates/backend/scripts/rollback.cjs +6 -3
  38. package/templates/backend/scripts/server.config.example.cjs +43 -15
  39. package/templates/backend/scripts/slowsql-report.mjs +2 -2
  40. package/templates/backend/src/common/api-log/api-log.service.ts +1 -1
  41. package/templates/backend/src/common/config-crypto.ts +133 -0
  42. package/templates/backend/src/config/config.interface.ts +22 -3
  43. package/templates/backend/src/config/configuration.ts +173 -86
  44. package/templates/backend/src/database/migrations/0001-init.migration.ts +1 -1
  45. package/templates/backend/src/database/slow-sql.logger.ts +1 -1
  46. package/templates/backend/src/database/sqlite.config.ts +2 -2
  47. package/templates/backend/src/main.ts +3 -3
  48. package/templates/backend/src/modules/ops/ops-probe.interface.ts +3 -3
  49. package/templates/backend/src/modules/ops/ops.module.ts +2 -11
  50. package/templates/backend/src/modules/user/user.service.ts +2 -2
  51. package/templates/docs/config-encryption.md +164 -0
  52. package/templates/root/AGENTS.md +5 -4
  53. package/templates/root/CLAUDE.md +1 -1
  54. package/templates/root/README.md +25 -15
  55. package/templates/root/_gitignore +5 -0
  56. package/templates/root/scripts/publish.cjs +20 -2
  57. package/templates/root/scripts/rollback.cjs +20 -2
  58. package/templates/web/config/router.config.ts +8 -0
  59. package/templates/web/package.json +5 -1
  60. package/templates/web/scripts/compress-src-images.cjs +332 -0
  61. package/templates/web/scripts/lib/interactive.cjs +134 -0
  62. package/templates/web/scripts/publish.cjs +299 -200
  63. package/templates/web/scripts/rollback.cjs +110 -41
  64. package/templates/web/scripts/server.config.example.json +17 -0
  65. package/templates/web/src/layouts/Background.module.less +0 -4
  66. package/templates/web/src/layouts/Background.tsx +48 -74
  67. package/templates/web/src/layouts/components/PageTabs/SortableTab.tsx +133 -0
  68. package/templates/web/src/layouts/components/PageTabs/index.module.less +105 -0
  69. package/templates/web/src/layouts/components/PageTabs/index.tsx +110 -0
  70. package/templates/web/src/layouts/components/PageTabs/usePageTabs.ts +224 -0
  71. package/templates/web/src/pages/Backend/Image/index.tsx +4 -4
  72. package/templates/web/src/pages/Maintenance/index.module.less +177 -0
  73. package/templates/web/src/pages/Maintenance/index.tsx +142 -0
  74. package/templates/web/src/services/api.ts +4 -0
  75. package/templates/web/src/services/app-request.ts +8 -1
  76. package/templates/web/src/services/maintenance.ts +67 -0
  77. package/templates/web/src/services/request.ts +5 -0
  78. package/transforms.js +21 -28
  79. package/templates/backend/ops/docker/.env.example +0 -40
  80. package/templates/backend/ops/sqlite/README.md +0 -57
  81. package/templates/backend/ops/sqlite/backup.sh +0 -65
  82. package/templates/backend/ops/sqlite/config.sh +0 -91
  83. package/templates/backend/ops/sqlite/healthcheck.sh +0 -25
  84. package/templates/backend/ops/sqlite/restore.sh +0 -105
  85. package/templates/backend/src/modules/ops/sqlite-backup-list.probe.ts +0 -100
  86. package/templates/backend/src/modules/ops/sqlite-backup-log.probe.ts +0 -63
  87. package/templates/backend/src/modules/ops/sqlite-backup.probe.ts +0 -144
@@ -1,6 +1,7 @@
1
1
  import Axios from 'axios';
2
2
  import { sha256 as hmacSha256 } from 'js-sha256';
3
3
  import ProjectConfig from '../../config/project.config';
4
+ import { enterMaintenance, isApiUnavailable } from './maintenance';
4
5
 
5
6
  /**
6
7
  * 大屏 API 请求实例(无 token、无过期,HMAC 签名)
@@ -160,7 +161,13 @@ appRequest.interceptors.response.use(
160
161
  // 后期响应加密时,在此先解密 base64 密文 → { code, message, data }
161
162
  return res.data;
162
163
  },
163
- (error) => Promise.reject(error)
164
+ (error) => {
165
+ // 后端不可用(502)→ 整页进入维护页;reject 照常透传给业务侧
166
+ if (isApiUnavailable(error)) {
167
+ enterMaintenance();
168
+ }
169
+ return Promise.reject(error);
170
+ }
164
171
  );
165
172
 
166
173
  export default appRequest;
@@ -0,0 +1,67 @@
1
+ import ProjectConfig from '../../config/project.config';
2
+
3
+ /**
4
+ * 维护态检测内核(纯 JS 模块,不依赖 React)
5
+ *
6
+ * 触发契约:自家后端接口(/api/**)返回 HTTP 502 —— 通常意味着 nginx 反代
7
+ * 后面的 backend 挂了或正在发版重启 —— 由 axios 拦截器调用 enterMaintenance(),
8
+ * 整页跳转到 /maintenance 维护页,并记录来源路径供恢复后跳回。
9
+ *
10
+ * 用整页跳转而非 SPA 路由切换:502 风暴时应用内状态已不可信,整页刷新
11
+ * 顺带清掉所有在途请求与旧页面状态;维护页自带 /api/version 探活自动恢复。
12
+ */
13
+
14
+ /** 来源路径在 sessionStorage 的键(隐私模式下存储可能抛错,一律 try/catch) */
15
+ const FROM_KEY = 'maintenance:from';
16
+
17
+ /** 维护页路由(挂在路由树顶层,含部署目录前缀) */
18
+ const MAINTENANCE_PATH = `${ProjectConfig.directory || ''}/maintenance`;
19
+
20
+ /** 跳转中标记:并发多个 502 只触发一次跳转 */
21
+ let redirecting = false;
22
+
23
+ /**
24
+ * 当前是否已处于维护页(防止维护页内的 502 再次触发跳转形成死循环)
25
+ */
26
+ export function isMaintenancePage(): boolean {
27
+ return window.location.pathname === MAINTENANCE_PATH;
28
+ }
29
+
30
+ /**
31
+ * 判定一个 axios 错误是否为「后端不可用(502)」
32
+ * 只认自家 /api 接口,第三方接口 502 不视为系统维护。
33
+ */
34
+ export function isApiUnavailable(error: any): boolean {
35
+ return (
36
+ error?.response?.status === 502 &&
37
+ typeof error?.config?.url === 'string' &&
38
+ error.config.url.includes('/api')
39
+ );
40
+ }
41
+
42
+ /**
43
+ * 进入维护页:记录来源路径 → 整页跳转
44
+ */
45
+ export function enterMaintenance(): void {
46
+ if (redirecting || isMaintenancePage()) return;
47
+ redirecting = true;
48
+ try {
49
+ sessionStorage.setItem(FROM_KEY, window.location.pathname + window.location.search);
50
+ } catch (e) {
51
+ // sessionStorage 不可用(隐私模式等)时放弃记录,恢复后回首页
52
+ }
53
+ window.location.href = MAINTENANCE_PATH;
54
+ }
55
+
56
+ /**
57
+ * 取出并清除来源路径(维护页恢复时跳回用)
58
+ */
59
+ export function consumeReturnPath(): string {
60
+ try {
61
+ const from = sessionStorage.getItem(FROM_KEY);
62
+ sessionStorage.removeItem(FROM_KEY);
63
+ return from && from !== MAINTENANCE_PATH ? from : '/';
64
+ } catch (e) {
65
+ return '/';
66
+ }
67
+ }
@@ -1,5 +1,6 @@
1
1
  import Axios from 'axios';
2
2
  import ProjectConfig from '../../config/project.config';
3
+ import { enterMaintenance, isApiUnavailable } from './maintenance';
3
4
 
4
5
  /**
5
6
  * 错误处理
@@ -42,6 +43,10 @@ axiosIntance.interceptors.response.use(
42
43
  return res.data;
43
44
  },
44
45
  (error) => {
46
+ // 后端不可用(502)→ 整页进入维护页;reject 照常透传给业务侧
47
+ if (isApiUnavailable(error)) {
48
+ enterMaintenance();
49
+ }
45
50
  errorHandle();
46
51
  return Promise.reject(error);
47
52
  }
package/transforms.js CHANGED
@@ -15,31 +15,30 @@
15
15
  /**
16
16
  * 按包管理器生成的命令替换规则(模板命令按 pnpm 写,npm/yarn 生成时改写)。
17
17
  * 三家在「workspace 过滤」上的等价形态:
18
- * pnpm: pnpm --filter <pkg> run <script>
19
- * npm: npm --workspace <pkg> run <script> (flag 可前置)
20
- * yarn1: yarn workspace <pkg> run <script> (子命令,非 flag)
18
+ * pnpm: pnpm --filter ./web run <script> (路径 filter,包名改名免疫)
19
+ * npm: npm --workspace ./web run <script> (flag 可前置;--workspace 同样认路径)
20
+ * yarn1: yarn workspace <pkg> run <script> (子命令,非 flag;只认包名 → 派生名)
21
21
  * 全 workspace 递归:
22
22
  * pnpm: pnpm -r [--parallel] run <script>
23
23
  * npm: npm run <script> --workspaces --if-present(顺序执行,无并行)
24
24
  * yarn1: yarn workspaces run <script>
25
25
  * 裸命令:npm 必须 `npm run <script>`,yarn 可直接 `yarn <script>`,pnpm 可直接。
26
26
  * 顺序敏感:复合长串先替换,裸 `pnpm ` 兜底(yarn 用)。
27
+ * pnpm 的 `--filter` 无匹配时退出码为 0(静默跳过)—— 模板 root 编排脚本已用 fs 预检
28
+ * 子包存在与 script 声明,勿只依赖退出码判断成败。
27
29
  */
28
30
  export function buildPmRules(pm, name) {
29
31
  if (pm === 'pnpm') {
30
- return [
31
- { find: '--filter web', replace: `--filter ${name}-web` },
32
- { find: '--filter backend', replace: `--filter ${name}-backend` },
33
- ];
32
+ return []; // 模板命令原生 pnpm,零改写(filter 已是 ./web 路径形态)
34
33
  }
35
34
  if (pm === 'npm') {
36
35
  return [
37
36
  // root scripts(templates/root/package.json 与文档)
38
37
  { find: 'pnpm -r --parallel run ', replace: 'npm run --workspaces --if-present ' },
39
38
  { find: 'pnpm -r run ', replace: 'npm run --workspaces --if-present ' },
40
- // 编排脚本(scripts/{publish,rollback}.cjs 的 spawn)与文档命令
41
- { find: 'pnpm --filter web', replace: `npm --workspace ${name}-web` },
42
- { find: 'pnpm --filter backend', replace: `npm --workspace ${name}-backend` },
39
+ // 编排脚本(scripts/{publish,rollback}.cjs 的 spawn)与文档命令(路径 filter 直译)
40
+ { find: 'pnpm --filter ./web', replace: 'npm --workspace ./web' },
41
+ { find: 'pnpm --filter ./backend', replace: 'npm --workspace ./backend' },
43
42
  // 裸命令(install 除外,npm 裸动词只有内建命令)
44
43
  { find: 'pnpm install', replace: 'npm install' },
45
44
  { find: 'pnpm dev', replace: 'npm run dev' },
@@ -52,12 +51,12 @@ export function buildPmRules(pm, name) {
52
51
  { find: 'pnpm monorepo', replace: 'npm monorepo' },
53
52
  ];
54
53
  }
55
- // yarn1(经典):动词直通 + workspaces 子命令
54
+ // yarn1(经典):动词直通 + workspaces 子命令;workspace 子命令只认包名 → 用派生名
56
55
  return [
57
56
  { find: 'pnpm -r --parallel run ', replace: 'yarn workspaces run ' },
58
57
  { find: 'pnpm -r run ', replace: 'yarn workspaces run ' },
59
- { find: 'pnpm --filter web', replace: `yarn workspace ${name}-web` },
60
- { find: 'pnpm --filter backend', replace: `yarn workspace ${name}-backend` },
58
+ { find: 'pnpm --filter ./web', replace: `yarn workspace ${name}-web` },
59
+ { find: 'pnpm --filter ./backend', replace: `yarn workspace ${name}-backend` },
61
60
  // publish:yarn 裸动词是内建发包命令,必须 run;rollback 顺带统一
62
61
  { find: 'pnpm publish', replace: 'yarn run publish' },
63
62
  { find: 'pnpm rollback', replace: 'yarn run rollback' },
@@ -90,26 +89,20 @@ export function buildTransforms(id, pm = 'pnpm') {
90
89
  // ── 通用规则(所有文本文件全局应用,顺序敏感:长串先替换) ──
91
90
  const globalRules = [
92
91
  // DB 文件名:template.<env>.sqlite → <dbPrefix>.<env>.sqlite
93
- // 出处:sqlite.config.ts:59 / configuration.ts:74-75 / data-source.ts:33 /
94
- // ops/sqlite/config.sh:39-40 / config.{develop,prod}.yaml:10 / docker-compose.yml:28
92
+ // 出处:sqlite.config.ts:57 / configuration.ts / scripts/data-source.ts:33 /
93
+ // config.interface.ts(注释)/ config.base.yaml(注释)/ docker-compose.yml
95
94
  { find: 'template.prod.sqlite', replace: `${dbPrefix}.prod.sqlite` },
96
95
  { find: 'template.dev.sqlite', replace: `${dbPrefix}.dev.sqlite` },
97
- // 模板字符串形态:ops 探针(sqlite-backup*.probe.ts)用 `template.${env}.` 前缀匹配
98
- { find: 'template.${env}.', replace: `${dbPrefix}.\${env}.` },
96
+ // 注释里的形态占位(如 config.interface.ts:16「定死为 template.<env>.sqlite」)
99
97
  { find: 'template.<env>.sqlite', replace: `${dbPrefix}.<env>.sqlite` },
100
- // 不带扩展名的 TAG/注释/示例形态:
101
- // - sqlite/config.sh:43 TAG 示例 template.prod;sqlite/README.md:25 同
102
- // - sqlite-backup-list.probe.ts:59 'template.prod'/'template.develop' 字面量
103
- // - sqlite-backup.probe.ts:139 dirName 示例 template.prod.<ts>
104
- { find: 'template.develop', replace: `${dbPrefix}.develop` },
105
- { find: 'template.prod', replace: `${dbPrefix}.prod` },
106
- { find: 'template.dev', replace: `${dbPrefix}.dev` },
107
- // 部署身份:容器名 / 服务名 / 镜像名 / APP_ROOT
108
- // 出处:docker-compose.yml:13,17 / install.sh:392 / docker/config.sh:17,19 /
109
- // systemd/config.sh:7 / pm2/config.sh:7 / restore.sh:103-105 / .env.example:6,7,11
98
+ // 部署身份:容器名 / 镜像名 / APP_ROOT
99
+ // 出处:docker-compose.yml(image + build args)/ Dockerfile ARG APP_LABEL 缺省(实际值构建期
100
+ // 由 ops/docker/install.sh --build-arg 注入;运行时 env 的 IMAGE/TAG 由 ops 派生对齐,
101
+ // .prod.docker.env 已是 buildops 自动生成物,不在模板内)
102
+ // (ops/ 脚本不再烙身份 —— 运行时由 ops/lib.sh::pkg_name 从 current/package.json 派生)
110
103
  { find: 'fullstack-template-backend', replace: containerName },
111
104
  // 服务器部署根目录
112
- // 出处:server.config.example.cjs:28 / ops/sqlite/config.sh:16 / sqlite/README.md:34,37,40,53
105
+ // 出处:server.config.example.cjs:28,37(targets 注释示例同串,一并替换)
113
106
  { find: '/data/server/fullstack-template', replace: appRoot },
114
107
  // 模板身份字面量(兜底,放最后;上面的长串已先被替换,不会误伤)
115
108
  { find: 'fullstack-template', replace: name },
@@ -1,40 +0,0 @@
1
- # docker compose 部署配置(复制为 .env 后改值)。
2
- # 镜像由 docker/install.sh 以 current/ 为 build context 现场构建 <IMAGE>:<TAG>,此处只引用 IMAGE/TAG。
3
- # 配置以环境变量注入(configuration.ts 读 env,缺省回退镜像内 yaml)。
4
-
5
- # 镜像名 & tag(tag = <version>-<14位ts>,每次部署一个新 tag,旧 tag 留作回滚)。
6
- # IMAGE 默认 fullstack-template-backend(与 docker/install.sh 的 config.sh 一致,通常不用改)。TAG 必填。
7
- IMAGE=fullstack-template-backend
8
- TAG=
9
-
10
- # 容器名。
11
- CONTAINER_NAME=fullstack-template-backend
12
-
13
- # 数据根目录(宿主路径,bind mount 进容器 /app/data,跨版本/镜像共享、轮转不动)。
14
- # db/backup/logs/uploads 全部落此根的子目录,后续追加新子目录无需再加 mount。必填。
15
- DATA_DIR=
16
-
17
- # 运行端口(docker 权威 = 此 PORT env;app 读 PORT 监听,宿主映射 = 容器监听 = PORT)。
18
- # 迁移新机只改此值即可换端口;systemd/pm2 模式端口权威在 config.prod.yaml,与此无关。
19
- PORT=3001
20
-
21
- # 业务配置(env override,空则用镜像内 config/prod.yaml 缺省)。
22
- # 注意:DB_DIR/UPLOAD_STORAGE_PATH 已在 docker run/compose 内置(随 /app/data mount 目标锁定),
23
- # 此处不再暴露,避免部署期改容器内路径致数据写飞(非持久化目录)。
24
- NODE_ENV=prod
25
- CORS_ORIGINS=
26
- # 前置 nginx 等可信反代时置 true(限流/审计 IP 按 x-forwarded-for 解析);直连暴露端口务必 false
27
- TRUST_PROXY=false
28
- # ⚠️ 模板占位:新项目务必换成自己的强密钥(与 config.prod.yaml / web/.env.production 同款)。
29
- # 种子化 .env 时一并带入,免部署期空值阻断。
30
- # 轮换:改本文件 + 重新部署,或直接改服务器 APP_ROOT/.env + 重启容器。
31
- JWT_SECRET=
32
- JWT_EXPIRES=8h
33
- LOG_LEVEL=warn
34
- ADMIN_PASSWORD=
35
- APP_SIGN_KEY=
36
- APP_SIGN_TS_WINDOW=300
37
- # 单点登录(默认关闭;接入时改 true 并配 ticketKey,见 README「单点登录」)
38
- SSO_ENABLED=false
39
- SSO_TICKET_KEY=
40
- SSO_TS_WINDOW=300
@@ -1,57 +0,0 @@
1
- # SQLite 运维工具(现场 shell 版)
2
-
3
- 不依赖 node、不进容器,运维现场可直接跑。脚本随 `backend/ops/sqlite/` 整目录发布到
4
- 宿主 `${backendServiceDir}/ops/sqlite/`(与 backend 发布版本解耦,跨版本共享)。
5
- 首次部署由运维手动把整目录拷到宿主并装 crontab(详见下文「与发布脚本的关系」)。
6
-
7
- ## 配置
8
-
9
- 改 `config.sh` 一处即可(`DATA_DIR`/`ENV`/`DB_NAME`/`KEEP`),所有脚本都 source 它。也可由参数覆盖:
10
-
11
- | 参数 | 说明 |
12
- |---|---|
13
- | `--db <path>` | 主库路径(默认 `${DATA_DIR}/${DB_NAME}`) |
14
- | `--env <prod\|develop>` | 环境,未显式设 `DB_NAME` 时按此推导文件名(prod→`template.prod.sqlite` / dev→`template.dev.sqlite`,与 app 代码定死的一致) |
15
- | `--keep <n>` | 保留份数(默认 7) |
16
- | `--data-dir <dir>` | 数据目录,派生 backup/logs 子目录 |
17
- | `--backup <dir>` | restore 指定回退的备份目录(不指定则取最近一份) |
18
- | `--no-recover` | restore 跳过 `.recover`,直接回退备份 |
19
-
20
- ### 主库文件名(由 app 定死,此处仅作运维定位)
21
-
22
- `DB_NAME` 默认按 `ENV` 推导为 `template.prod.sqlite`(prod)/ `template.dev.sqlite`(dev),
23
- 与 app 代码(`sqlite.config.ts` / `data-source.ts`)定死的文件名一致——文件名不外配,
24
- 仅目录(`DB_DIR`/`backendDataDir`)可配。备份目录名随之用 `<tag>.<ts>`
25
- (`<tag>` = `DB_NAME` 去扩展名:`template.prod.sqlite → template.prod`)。
26
-
27
- 改 `config.sh` 的 `DB_NAME` 可显式覆盖(如排障指向特定文件),但默认值勿改——
28
- 须与 app 代码定死的文件名一致,否则脚本找不到主库。
29
-
30
- ## 三件套
31
-
32
- ```bash
33
- # 定时在线备份(cron 与手动均用):sqlite3 .backup 产单文件自洽快照(WAL 已并入)
34
- ./backup.sh --db /data/server/fullstack-template/data/template.prod.sqlite --env prod --keep 7
35
-
36
- # 损坏快速切换:留现场 → 优先 .recover 抢救 → 回退备份;不重启进程,打印重启命令
37
- ./restore.sh --db /data/server/fullstack-template/data/template.prod.sqlite --env prod
38
-
39
- # 健康探针:文件头魔数判定,不依赖 sqlite3 CLI;exit 0 健康 / 1 异常
40
- ./healthcheck.sh --db /data/server/fullstack-template/data/template.prod.sqlite
41
- ```
42
-
43
- 异常自动切换:`./healthcheck.sh --db <db> --env prod || ./restore.sh --db <db> --env prod`
44
-
45
- ## 产物与日志(与 ops 页探针同源,勿改)
46
-
47
- - 备份目录:`${DATA_DIR}/backup/<tag>.<ts>/<basename>`(单文件自洽主库)
48
- - 日志文件:`${DATA_DIR}/logs/sqlite-backup.log`(ops 页 `SqliteBackupLogProbe` 读尾 50 行;`❌` 行被判异常,故脚本仅真失败打 `❌`)
49
-
50
- ## 与发布脚本的关系
51
-
52
- - **定时备份(cron)**:本目录 `backup.sh`(在线 `.backup`),由宿主 crontab 调用。
53
- 首次部署运维手动装:`crontab -e` 加一行(如 `0 3 * * * /data/server/fullstack-template/ops/sqlite/backup.sh --env prod --keep 7 >/dev/null 2>&1`)。
54
- - **迁移前备份**:原 `scripts/lib/sqlite-backup.cjs`(node cp 三件套)随旧管线删除,不再自动跑。
55
- 需时手动触发:`./backup.sh --env prod`(在线 `.backup`,WAL 已并入,产物单文件自洽)。
56
- restore 兼容历史 cp 三件套产物与 `.backup` 单文件产物(旁文件存在才拷)。
57
- - 根 `scripts/sqlite-ops.cjs` 早已废弃删除,运维现场统一用本目录 shell 版。
@@ -1,65 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SQLite 在线备份 —— 宿主 sqlite3 .backup 产单文件自洽快照(WAL 已并入),不依赖 node。
3
- # cron 与手动均用此脚本;产物落 ${DATA_DIR}/backup/${TAG}.<ts>/<basename>,按 KEEP 轮转。
4
- # 日志 >> ${DATA_DIR}/logs/sqlite-backup.log(ops 页 SqliteBackupLogProbe 读此文件;❌ 行被判异常,故仅真失败打 ❌)。
5
- #
6
- # 为什么用 .backup 而非 cp 三件套:
7
- # .backup 是 SQLite 在线一致性快照接口,拷贝期间即使 app 在写也产出自洽单文件副本,
8
- # 不存在 cp 三件套"拷到一半正好有写事务"的中间态风险。docker 下后台随时可能有人配置台写,
9
- # 无低峰保证,故定时备份必须走 .backup。产物单文件自洽(无 -wal/-shm),restore 已兼容。
10
- # 迁移前备份也改用本脚本(在线 .backup 单文件自洽),或停 app 后 cp 三件套;旧 deploy 的 node lib/sqlite-backup.cjs 随管线删除,不再自动跑。
11
- #
12
- # 用法:
13
- # ./backup.sh # 用 config.sh 默认值
14
- # ./backup.sh --db <path> --env prod --keep 7 --data-dir <dir>
15
- # cron: 0 3 * * * /.../ops/sqlite/backup.sh --db ... --env prod --keep 7 --data-dir ... >/dev/null 2>&1
16
- set -euo pipefail
17
- DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
- source "$DIR/config.sh"
19
-
20
- while [[ $# -gt 0 ]]; do
21
- case "$1" in
22
- --db) DB="$2"; shift 2 ;;
23
- --env) ENV="$2"; shift 2 ;;
24
- --keep) KEEP="$2"; shift 2 ;;
25
- --data-dir) DATA_DIR="$2"; shift 2 ;;
26
- *) echo "未知参数: $1" >&2; exit 2 ;;
27
- esac
28
- done
29
- sqlite_derive
30
- BASENAME="$(basename "$DB")"
31
-
32
- # ── 前置检查 ──
33
- if ! sqlite_has_cli; then
34
- sqlite_log "❌ 宿主无 sqlite3,备份无法执行(DB=$DB)。请装 sqlite3 或改方案。"
35
- exit 1
36
- fi
37
- if [[ ! -f "$DB" ]]; then
38
- sqlite_log "⚠️ 主库不存在($DB),首次部署跳过。"
39
- exit 0
40
- fi
41
-
42
- DEST_DIR="${BACKUP_DIR}/${TAG}.$(_ts)"
43
- DEST_FILE="${DEST_DIR}/${BASENAME}"
44
- mkdir -p "$DEST_DIR"
45
-
46
- # ── 在线一致性备份 ──
47
- sqlite_log "▶ 备份开始:sqlite3 \"$DB\" .backup → $DEST_FILE"
48
- if sqlite3 "$DB" ".backup '$DEST_FILE'" 2>>"$LOG_FILE"; then
49
- if sqlite_is_valid "$DEST_FILE"; then
50
- SIZE=$(stat -c '%s' "$DEST_FILE" 2>/dev/null || stat -f '%z' "$DEST_FILE")
51
- sqlite_log "✅ 备份完成:$DEST_FILE (${SIZE}B,单文件自洽,WAL 已并入)"
52
- else
53
- sqlite_log "❌ 备份产物非有效 SQLite(魔数不符),已清理:$DEST_FILE"
54
- rm -f "$DEST_FILE"; rmdir "$DEST_DIR" 2>/dev/null || true
55
- exit 1
56
- fi
57
- else
58
- sqlite_log "❌ sqlite3 .backup 失败(见日志上行 stderr),已清理半成品:$DEST_FILE"
59
- rm -f "$DEST_FILE"; rmdir "$DEST_DIR" 2>/dev/null || true
60
- exit 1
61
- fi
62
-
63
- # ── 轮转 ──
64
- sqlite_rotate "$BACKUP_DIR" "$TAG" "$KEEP"
65
- exit 0
@@ -1,91 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SQLite 运维共用配置 —— 运维按环境改这一处即可,backup/restore/healthcheck 都 source 它。
3
- # 与 backend/ops/{docker,systemd,pm2} 的 config.sh 同款:绝对路径、${VAR:-default} env override。
4
- #
5
- # 配置项(DATA_DIR/ENV/DB_NAME/KEEP)既可在此改,也可由 deploy cron 行或手动 --db/--env/--keep/--data-dir 覆盖。
6
- # 不重新打包服务:这些脚本只读 data 目录做文件操作,改值 + 重跑脚本即生效,与 backend 发布解耦。
7
- #
8
- # 约定(与 deploy 迁移前备份、ops 页探针保持一致,勿改):
9
- # - 备份目录:${DATA_DIR}/backup/${TAG}.<ts>/ (单文件自洽主库,WAL 已并入)
10
- # - 日志文件:${DATA_DIR}/logs/sqlite-backup.log (ops 页 SqliteBackupLogProbe 读此文件尾;
11
- # ❌ 行被探针判异常,故各脚本只在真失败时打 ❌,成功打 ✅)
12
-
13
- # 环境(develop|prod,决定 DB_NAME/备份目录名 <env>.sqlite / <env>.<ts>)
14
- ENV="prod"
15
- # 数据目录(sqlite + uploads + backup + logs 所在;docker 下即 data volume 源=宿主真实目录)
16
- DATA_DIR="/data/server/fullstack-template/data"
17
- # 主库文件名(留空则 sqlite_derive 按 ENV 推导,与 app 代码定死的文件名一致:
18
- # prod=template.prod.sqlite / dev=template.dev.sqlite;--db 显式覆盖)。
19
- # 文件名由 app 定死不外配,此处仅作运维定位/备份 tag 用,改值须与 app 代码同步。
20
- DB_NAME=""
21
- # 主库路径(留空则由 sqlite_derive 推导为 ${DATA_DIR}/${DB_NAME};--db 显式覆盖)
22
- DB=""
23
- # 备份保留份数
24
- KEEP=7
25
-
26
- # ── 派生路径(脚本解析完参数后调 sqlite_derive 一次性算定)──
27
- BACKUP_DIR=""
28
- LOG_DIR=""
29
- LOG_FILE=""
30
- TAG=""
31
-
32
- sqlite_derive() {
33
- BACKUP_DIR="${DATA_DIR}/backup"
34
- LOG_DIR="${DATA_DIR}/logs"
35
- LOG_FILE="${LOG_DIR}/sqlite-backup.log"
36
- # DB_NAME 未显式设定则按 ENV 推导,与 app 代码定死的文件名一致:
37
- # prod → template.prod.sqlite / dev → template.dev.sqlite(--env 覆盖 ENV 后此处同步生效)。
38
- if [[ -z "$DB_NAME" ]]; then
39
- if [[ "$ENV" == "prod" ]]; then DB_NAME="template.prod.sqlite";
40
- else DB_NAME="template.dev.sqlite"; fi
41
- fi
42
- # TAG = DB 文件名去扩展名,用作备份目录名前缀(<tag>.<ts>)与轮转/回退 glob。
43
- # DB_NAME=template.prod.sqlite → TAG=template.prod;DB_NAME=prod.sqlite → TAG=prod。
44
- TAG="${DB_NAME%.*}"
45
- # 注意:用 if/then 而非 [[ -z ]] && DB=... —— 后者在 DB 非空时整句返回 1,
46
- # 在 set -e 下会让 sqlite_derive 返回 1 致脚本中途退出。
47
- if [[ -z "$DB" ]]; then DB="${DATA_DIR}/${DB_NAME}"; fi
48
- }
49
-
50
- # 时间戳:目录名用 YYYYMMDD-HHMMSS(与 releases/<ts> 同款,字典序≈时间序)
51
- _ts() { date '+%Y%m%d-%H%M%S'; }
52
- _now() { date '+%Y-%m-%d %H:%M:%S'; }
53
-
54
- # 日志:同时落 LOG_FILE + stdout。cron 行重定向 stdout→/dev/null,LOG_FILE 留记录;手动跑两边都见。
55
- # LOG_FILE 不可写(目录缺失/权限)时只打 stdout,不致脚本失败。
56
- sqlite_log() {
57
- local msg="[$(_now)] $*"
58
- echo "$msg"
59
- if [[ -n "${LOG_FILE:-}" ]]; then
60
- mkdir -p "$LOG_DIR" 2>/dev/null || true
61
- printf '%s\n' "$msg" >> "$LOG_FILE" 2>/dev/null || true
62
- fi
63
- }
64
-
65
- # 宿主是否有 sqlite3 CLI
66
- sqlite_has_cli() { command -v sqlite3 >/dev/null 2>&1; }
67
-
68
- # 文件头魔数判定(SQLite 文件头 15 字节 = "SQLite format 3",第 16 字节为 NUL)。
69
- # 只比首 15 字节的可打印串,避开 NUL —— 兼容 GNU/BSD grep 与 macOS/Linux/macOS sh。
70
- # (旧法 head -c 16 | grep -q $'...\x00' 在 BSD grep 下匹配 NUL 失败,故弃用。)
71
- sqlite_is_valid() {
72
- local f="$1"
73
- [[ -f "$f" ]] || return 1
74
- [[ "$(head -c 15 "$f")" = "SQLite format 3" ]]
75
- }
76
-
77
- # 备份轮转:按目录名时间戳倒序,删超 keep 份。
78
- # while 在子 shell 里跑(管道),计数取不回主 shell,故另用 ls|wc 统计删除数。
79
- sqlite_rotate() {
80
- local bdir="$1" tag="$2" keep="$3"
81
- local i=0 total deleted
82
- ls -1d "${bdir}"/${tag}.* 2>/dev/null | sort -r | while read -r d; do
83
- i=$((i + 1))
84
- # if/then 而非 [[ ]] && rm:i<=keep 时整句返回 1 会触发 set -e 退出子 shell 致轮转中断
85
- if [[ "$i" -gt "$keep" ]]; then rm -rf "$d"; fi
86
- done
87
- total=$(ls -1d "${bdir}"/${tag}.* 2>/dev/null | wc -l | tr -d ' ')
88
- deleted=$((total - keep))
89
- # if/then 而非 [[ ]] && sqlite_log,避免 deleted<=0 时 set -e 中途退出
90
- if [[ "$deleted" -gt 0 ]]; then sqlite_log "🧹 轮转:保留 $keep 份(当前共 $total 份),已删 $deleted 份旧备份"; fi
91
- }
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SQLite 健康探针 —— 文件头魔数("SQLite format 3\0")判定,不依赖 sqlite3 CLI。
3
- # 返回 0=健康,1=异常。供 cron/supervisor 主动检测;异常可串 restore 自动切换:
4
- # ./healthcheck.sh --db <db> --env prod || ./restore.sh --db <db> --env prod
5
- set -euo pipefail
6
- DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
- source "$DIR/config.sh"
8
-
9
- while [[ $# -gt 0 ]]; do
10
- case "$1" in
11
- --db) DB="$2"; shift 2 ;;
12
- --env) ENV="$2"; shift 2 ;;
13
- --data-dir) DATA_DIR="$2"; shift 2 ;;
14
- *) echo "未知参数: $1" >&2; exit 2 ;;
15
- esac
16
- done
17
- sqlite_derive
18
-
19
- if sqlite_is_valid "$DB"; then
20
- echo "✓ 健康: $DB"
21
- exit 0
22
- else
23
- echo "✗ 异常: $DB(文件不存在或非 SQLite 格式)" >&2
24
- exit 1
25
- fi
@@ -1,105 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SQLite 损坏快速切换(restore)—— 留现场 + 优先 .recover 抢救 + 回退备份,RTO 秒级。
3
- # 不重启进程(进程重启由 supervisor 负责),结束时打印 pm2/systemd/docker 重启命令。
4
- #
5
- # 流程:
6
- # 1) 移走损坏主库(连 -wal/-shm 一起)为 <db>.corrupt.<ts>,留现场不删。
7
- # 2) 若有 sqlite3 CLI 且未 --no-recover:.recover 抢救残页 → .sql → 导入新库 → 魔数校验。
8
- # 3) .recover 失败/跳过/产物无效 → 回退到最近一份备份目录(或 --backup <dir> 指定):
9
- # 主库 + -wal/-shm(存在才拷)一起 cp 回原位。
10
- # 4) .recover 成功路径兜底清 -wal/-shm(重建库本无旁文件);备份还原路径旁文件已就位不动。
11
- # 5) 不重启进程,打印重启命令。
12
- #
13
- # 用法:
14
- # ./restore.sh --db <损坏主库> [--env prod] [--backup <备份目录>] [--no-recover] [--data-dir <dir>]
15
- set -euo pipefail
16
- DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
- source "$DIR/config.sh"
18
-
19
- NO_RECOVER=0
20
- BACKUP_ROOT=""
21
- while [[ $# -gt 0 ]]; do
22
- case "$1" in
23
- --db) DB="$2"; shift 2 ;;
24
- --env) ENV="$2"; shift 2 ;;
25
- --backup) BACKUP_ROOT="$2"; shift 2 ;;
26
- --no-recover) NO_RECOVER=1; shift ;;
27
- --data-dir) DATA_DIR="$2"; shift 2 ;;
28
- *) echo "未知参数: $1" >&2; exit 2 ;;
29
- esac
30
- done
31
- sqlite_derive
32
- BASENAME="$(basename "$DB")"
33
- mkdir -p "$LOG_DIR" "$BACKUP_DIR"
34
-
35
- [[ -f "$DB" ]] || { echo "✗ 主库不存在: $DB" >&2; exit 1; }
36
- TS=$(_ts)
37
- CORRUPT="${DB}.corrupt.${TS}"
38
-
39
- # 1) 留现场:损坏主库 + 旁文件一起移走
40
- echo "▶ 移走损坏主库 → $CORRUPT"
41
- mv "$DB" "$CORRUPT"
42
- mv "${DB}-wal" "${CORRUPT}-wal" 2>/dev/null || true
43
- mv "${DB}-shm" "${CORRUPT}-shm" 2>/dev/null || true
44
-
45
- # 2) 优先 .recover 抢救(需 sqlite3 CLI)。注意:.recover 产物是 SQL 文本,
46
- # 不能 `sqlite3 corrupt ".recover" > db`(非有效库),须先落 .sql 再导入空库重建。
47
- RESTORED=0
48
- if sqlite_has_cli && [[ "$NO_RECOVER" -eq 0 ]]; then
49
- SQL="${DB}.recovered.${TS}.sql"
50
- echo "▶ .recover 抢救残页 → $SQL"
51
- if sqlite3 "$CORRUPT" ".recover" > "$SQL" 2>>"$LOG_FILE"; then
52
- echo "▶ 导入 $SQL → $DB"
53
- if sqlite3 "$DB" < "$SQL" 2>>"$LOG_FILE" && sqlite_is_valid "$DB"; then
54
- echo "✓ .recover 抢救成功 → $DB"
55
- rm -f "$SQL"
56
- RESTORED=1
57
- else
58
- echo "⚠ .recover 产物导入/校验失败,回退备份" >&2
59
- rm -f "$SQL" "$DB"
60
- fi
61
- else
62
- echo "⚠ .recover 失败,清理半成品后回退备份" >&2
63
- rm -f "$SQL" "$DB"
64
- fi
65
- elif ! sqlite_has_cli; then
66
- echo "⚠ 无 sqlite3 CLI,跳过 .recover,直接回退备份" >&2
67
- fi
68
-
69
- # 3) 回退到最近三件套备份目录
70
- if [[ "$RESTORED" -eq 0 ]]; then
71
- if [[ -z "$BACKUP_ROOT" ]]; then
72
- BACKUP_ROOT=$(ls -1d "${BACKUP_DIR}"/${TAG}.* 2>/dev/null | sort -r | head -1)
73
- fi
74
- BACKUP_MAIN="${BACKUP_ROOT}/${BASENAME}"
75
- if [[ -z "$BACKUP_ROOT" || ! -f "$BACKUP_MAIN" ]]; then
76
- echo "✗ 无可用备份(${BACKUP_DIR}/${TAG}.* 目录)。损坏主库留于 $CORRUPT,需手动处理。" >&2
77
- sqlite_log "❌ restore 失败:无可用备份,损坏主库留于 $CORRUPT"
78
- exit 1
79
- fi
80
- echo "▶ 回退备份 $BACKUP_MAIN → $DB"
81
- rm -f "$DB" && cp -a "$BACKUP_MAIN" "$DB"
82
- for ext in -wal -shm; do
83
- src="${BACKUP_ROOT}/${BASENAME}${ext}"
84
- # if/then 而非 [[ -f ]] && { ... },旁文件不存在时整句返回 1 会触发 set -e 退出
85
- if [[ -f "$src" ]]; then
86
- rm -f "${DB}${ext}" && cp -a "$src" "${DB}${ext}"
87
- fi
88
- done
89
- echo "✓ 已从备份还原: $DB"
90
- fi
91
-
92
- # 4) .recover 成功路径清残留 -wal/-shm;备份还原路径旁文件已就位不动
93
- if [[ "$RESTORED" -eq 1 ]]; then
94
- rm -f "${DB}-wal" "${DB}-shm"
95
- echo "✓ 清理 .recover 残留 -wal/-shm"
96
- fi
97
-
98
- sqlite_log "✅ restore 完成:$DB(recover=${RESTORED},现场=$CORRUPT)"
99
- echo
100
- echo "✓ 主库已就绪: $DB"
101
- echo " 损坏现场保留: $CORRUPT(事后可再 .recover 或取证后删除)"
102
- echo " 请重启进程使新库生效:"
103
- echo " pm2: pm2 restart fullstack-template-backend"
104
- echo " systemd: systemctl restart fullstack-template-backend"
105
- echo " docker: docker restart fullstack-template-backend"