@covro/covro 0.2.1-dev.6 → 0.2.1-dev.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.
- package/README.md +7 -0
- package/README.zh-CN.md +7 -0
- package/dist/backups/factory.js +22 -0
- package/dist/backups/factory.js.map +7 -0
- package/dist/backups/lock.js +22 -0
- package/dist/backups/lock.js.map +7 -0
- package/dist/backups/manager.js +170 -0
- package/dist/backups/manager.js.map +7 -0
- package/dist/backups/restore.js +188 -0
- package/dist/backups/restore.js.map +7 -0
- package/dist/backups/store.js +274 -0
- package/dist/backups/store.js.map +7 -0
- package/dist/cli/backup.js +84 -0
- package/dist/cli/backup.js.map +7 -0
- package/dist/cli/daemon.js +7 -0
- package/dist/cli/daemon.js.map +2 -2
- package/dist/cli/main.js +35 -5
- package/dist/cli/main.js.map +2 -2
- package/dist/cli/paths.js.map +2 -2
- package/dist/server/db/history.js +60 -0
- package/dist/server/db/history.js.map +7 -0
- package/dist/server/db/index.js +11 -1
- package/dist/server/db/index.js.map +2 -2
- package/dist/server/db/migrate.js +8 -18
- package/dist/server/db/migrate.js.map +2 -2
- package/dist/server/http.js +21 -3
- package/dist/server/http.js.map +2 -2
- package/dist/server/main.js +14 -3
- package/dist/server/main.js.map +2 -2
- package/dist/server/service.js +9 -5
- package/dist/server/service.js.map +2 -2
- package/dist/shared/backups.js +55 -0
- package/dist/shared/backups.js.map +7 -0
- package/dist/shared/database.js +15 -0
- package/dist/shared/database.js.map +7 -0
- package/dist/update-worker.cjs +934 -388
- package/dist/updates/files.js +2 -1
- package/dist/updates/files.js.map +2 -2
- package/dist/updates/transaction.js +3 -24
- package/dist/updates/transaction.js.map +2 -2
- package/dist/updates/worker.js +69 -19
- package/dist/updates/worker.js.map +2 -2
- package/dist/web/assets/{abnfDiagram-WEYOIA3C-BkC1EVBk.js → abnfDiagram-WEYOIA3C-Baw8Q2RL.js} +1 -1
- package/dist/web/assets/{arc-CYGT9huc.js → arc-BvZHXzPr.js} +1 -1
- package/dist/web/assets/{architecture-WOLXFQ4H-DCsF3gVg.js → architecture-WOLXFQ4H-DrQ2OxuY.js} +1 -1
- package/dist/web/assets/{architectureDiagram-ZHVYKT53-Cn6kovuz.js → architectureDiagram-ZHVYKT53-D96hX75H.js} +1 -1
- package/dist/web/assets/{blockDiagram-RJAE7QA3-CNk2zXm9.js → blockDiagram-RJAE7QA3-Csce-aWb.js} +1 -1
- package/dist/web/assets/{c4Diagram-XDPPPE54-CTuI_1XJ.js → c4Diagram-XDPPPE54-Dfu8ZQ6Y.js} +1 -1
- package/dist/web/assets/channel-DQhtAfM4.js +1 -0
- package/dist/web/assets/{chunk-2E7WHVMD-TrYs0qV3.js → chunk-2E7WHVMD-Vb63e-lu.js} +1 -1
- package/dist/web/assets/{chunk-4EMNKR2T-QL4-PZ4r.js → chunk-4EMNKR2T-DrY1P7q3.js} +1 -1
- package/dist/web/assets/{chunk-55XCU76K-yynn-EEr.js → chunk-55XCU76K-D70bjQ9E.js} +1 -1
- package/dist/web/assets/{chunk-7S6XSTFO-dzrEn-_W.js → chunk-7S6XSTFO-CfW90MUC.js} +1 -1
- package/dist/web/assets/{chunk-AQDTSKZB-DaNZMEGO.js → chunk-AQDTSKZB-u4KDDGBt.js} +1 -1
- package/dist/web/assets/{chunk-DSNLSHJZ-BnMtx8Ho.js → chunk-DSNLSHJZ-Cax3giJD.js} +1 -1
- package/dist/web/assets/{chunk-HAGFOTED-fg-JyiA5.js → chunk-HAGFOTED-BG9UMwzK.js} +1 -1
- package/dist/web/assets/{chunk-HNE4Q36P-CCsfPbcn.js → chunk-HNE4Q36P-BNxIfOgZ.js} +1 -1
- package/dist/web/assets/{chunk-NQOKRVIF-DLlOpcWZ.js → chunk-NQOKRVIF-DJ60e0Al.js} +1 -1
- package/dist/web/assets/{chunk-UHW7PFBP-B-sPKpeS.js → chunk-UHW7PFBP-C9gHYcjJ.js} +1 -1
- package/dist/web/assets/{chunk-VQ5W62QI-BVqGMzey.js → chunk-VQ5W62QI-TyHPGVfQ.js} +1 -1
- package/dist/web/assets/{chunk-WQQL4CDK-DcwJXNuh.js → chunk-WQQL4CDK-DeAauheh.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-DRwLkqyU.js → chunk-XXDRQBXY-V-L20Tbi.js} +1 -1
- package/dist/web/assets/{chunk-YZO67HFG-BrrgBnrQ.js → chunk-YZO67HFG-LV5QVBiU.js} +1 -1
- package/dist/web/assets/{chunk-ZAUX2LTX-C7qvwkRr.js → chunk-ZAUX2LTX-CBsBBk7L.js} +1 -1
- package/dist/web/assets/{chunk-ZCHMQSOR-DdBd3Cvs.js → chunk-ZCHMQSOR-a2_9JHHn.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-4MYWR3UA-Dn2RZb3v.js → classDiagram-v2-4MYWR3UA-77eiBCO3.js} +1 -1
- package/dist/web/assets/{cose-bilkent-JH36ORCC-BlXvtfgE.js → cose-bilkent-JH36ORCC-DDP73pnY.js} +1 -1
- package/dist/web/assets/{cynefin-EF2NZ3EQ-odDgTxc-.js → cynefin-EF2NZ3EQ-onpnpyxc.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-WF24UDRP-BySEoCW8.js → cynefinDiagram-WF24UDRP-BmLplbnH.js} +1 -1
- package/dist/web/assets/{dagre-NK4GIZVX-ihxmhB4X.js → dagre-NK4GIZVX-BhaMf_Gx.js} +1 -1
- package/dist/web/assets/{diagram-CBK3MWTC-Ckr5P3IM.js → diagram-CBK3MWTC-BaalTqFt.js} +1 -1
- package/dist/web/assets/{diagram-I5XIIV2D-D5BZIval.js → diagram-I5XIIV2D-4Tdshcyt.js} +1 -1
- package/dist/web/assets/{diagram-ODIL2UCW-BC5jjqqY.js → diagram-ODIL2UCW-SlEcZgod.js} +1 -1
- package/dist/web/assets/{diagram-PFBEJUXE-D_IdYKqC.js → diagram-PFBEJUXE-CosCAXbh.js} +1 -1
- package/dist/web/assets/{diagram-RJA3I4VV-DlPNsPqP.js → diagram-RJA3I4VV-HQElVL4B.js} +1 -1
- package/dist/web/assets/{diagram-TYUNFSNR-Ca2ANhGM.js → diagram-TYUNFSNR-BT2WDc21.js} +1 -1
- package/dist/web/assets/{dist-BSWkYZRr.js → dist-CrpGQKqw.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-V4P3TTPT-BrqvmeiB.js → ebnfDiagram-V4P3TTPT-BhK6AM0_.js} +1 -1
- package/dist/web/assets/{elk-QONTBCTQ-BfXeOE2E.js → elk-QONTBCTQ-CS7DfCnD.js} +1 -1
- package/dist/web/assets/{erDiagram-KYQIGQAA-CMuYoCVJ.js → erDiagram-KYQIGQAA-DxVnuqFK.js} +1 -1
- package/dist/web/assets/{eventmodeling-K75KTNOO-9s4vQqv1.js → eventmodeling-K75KTNOO-oC_YeomD.js} +1 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-Bspg0rQz.js +1 -0
- package/dist/web/assets/{ganttDiagram-R7GPNTJY-B-Xmfmm2.js → ganttDiagram-R7GPNTJY-CquUpiE1.js} +1 -1
- package/dist/web/assets/{gitGraph-VSP46ZUC-ZSbSQ7jf.js → gitGraph-VSP46ZUC-BTtGQYsh.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-BhxijLaZ.js → gitGraphDiagram-PDYL7XHY-BsokGXYo.js} +1 -1
- package/dist/web/assets/index-B8tsymVC.js +147 -0
- package/dist/web/assets/{index-mkDvqMQ3.css → index-COSho7m2.css} +1 -1
- package/dist/web/assets/{info-OHQRW6UA-CTSCkjt3.js → info-OHQRW6UA-CvGN4gWh.js} +1 -1
- package/dist/web/assets/{infoDiagram-MVDRKAX5-D46MTesD.js → infoDiagram-MVDRKAX5-BF9Rp3BT.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-Bafu9NOk.js → ishikawaDiagram-HPEHV2GE-DtD20ngU.js} +1 -1
- package/dist/web/assets/{journeyDiagram-BOSHW7O7-CRsIi1Ry.js → journeyDiagram-BOSHW7O7-gB16trHV.js} +1 -1
- package/dist/web/assets/{kanban-definition-FNSA4HC5-DjIT_uLs.js → kanban-definition-FNSA4HC5-D0aKOsE2.js} +1 -1
- package/dist/web/assets/{line-B5MDdgVs.js → line-BR5dqPmF.js} +1 -1
- package/dist/web/assets/{linear-wm7JWxlT.js → linear-OsiQaPpK.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-C7jTW4sZ.js → mermaid-parser.core-MEuwYVot.js} +3 -3
- package/dist/web/assets/{mermaid.core-DcWRGjP5.js → mermaid.core-CVKnnz65.js} +4 -4
- package/dist/web/assets/{mindmap-definition-6OY57UG3-qY6vV0UI.js → mindmap-definition-6OY57UG3-5aHA-kPJ.js} +1 -1
- package/dist/web/assets/{packet-JDAUHWVQ-9B7vLYK0.js → packet-JDAUHWVQ-uj48AF5r.js} +1 -1
- package/dist/web/assets/{pegDiagram-ECIUJNBX-B7gLZ_Fs.js → pegDiagram-ECIUJNBX-BkVIHLBX.js} +1 -1
- package/dist/web/assets/{pie-XZMESJXO-MfazZSiE.js → pie-XZMESJXO-D3hy3nD7.js} +1 -1
- package/dist/web/assets/{pieDiagram-GNMPY74T-1ahagzzC.js → pieDiagram-GNMPY74T-CBAqGMEo.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-D57K4MC3-C-GkwjOp.js → quadrantDiagram-D57K4MC3-C35kvNRB.js} +1 -1
- package/dist/web/assets/{radar-ABXABTNO-CgutvWTi.js → radar-ABXABTNO-3LWE6J2Y.js} +1 -1
- package/dist/web/assets/{railroad-I3PHUGI6-Bl9LJINm.js → railroad-I3PHUGI6-B6Mc9Djl.js} +1 -1
- package/dist/web/assets/{railroad-abnf-LNEFI6M7-CGRpAnuU.js → railroad-abnf-LNEFI6M7-DJnbwbpn.js} +1 -1
- package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-D6A47xZS.js → railroad-ebnf-DWYD2UWJ-BFXAvY4k.js} +1 -1
- package/dist/web/assets/{railroad-peg-JR7BVNR7-DxGphrFO.js → railroad-peg-JR7BVNR7-JYGb2Oq5.js} +1 -1
- package/dist/web/assets/{railroadDiagram-Y72NRBL7-D8P17OV4.js → railroadDiagram-Y72NRBL7-uBIggR0w.js} +1 -1
- package/dist/web/assets/{requirementDiagram-EQYSW5RA-c93_xAaQ.js → requirementDiagram-EQYSW5RA-CDWglrxJ.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-SY24JHO5-FankqwcZ.js → sankeyDiagram-SY24JHO5-BT16hnTl.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-BLEHNHQU-CWH5SeL1.js → sequenceDiagram-BLEHNHQU-eFJ27rVk.js} +1 -1
- package/dist/web/assets/{src-BXQOmVZv.js → src-BlOCBHYS.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-DLfcw0e7.js → stateDiagram-v2-A6IESKWZ-CK_s-2Qv.js} +1 -1
- package/dist/web/assets/{swimlanes-B3ENRKFQ-DZ-yBiap.js → swimlanes-B3ENRKFQ-WLMrk9Lm.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-AYQRV4AK-DmjebfnI.js +8 -0
- package/dist/web/assets/{timeline-definition-JXYKGLGB-B7XvxDHY.js → timeline-definition-JXYKGLGB-D0-xZS_V.js} +1 -1
- package/dist/web/assets/{treeView-D4JQ5SDB-DrB6wXJ0.js → treeView-D4JQ5SDB-TTivyB8T.js} +1 -1
- package/dist/web/assets/{treemap-SAJKECNS-BynFjsvx.js → treemap-SAJKECNS-DtU16wOO.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-WPOLBNNX-BISzHOHN.js → usecaseDiagram-WPOLBNNX-DLFybW0F.js} +1 -1
- package/dist/web/assets/{vennDiagram-Q3Y3IDCW-BZwjTIST.js → vennDiagram-Q3Y3IDCW-DRDuYw70.js} +1 -1
- package/dist/web/assets/{wardley-7MLQ67FV-2bM0pMhO.js → wardley-7MLQ67FV-tZ-5qrqv.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-IVAIDKWA-CflkU8us.js → wardleyDiagram-IVAIDKWA-wRKeGCMl.js} +1 -1
- package/dist/web/assets/{xychartDiagram-4URNHDU4-DRspOlD3.js → xychartDiagram-4URNHDU4-B5-k-DHF.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +8 -3
- package/dist/web/assets/channel-BDxsxdm-.js +0 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-BHwP7Ha7.js +0 -1
- package/dist/web/assets/index-BrvtlTwN.js +0 -147
- package/dist/web/assets/swimlanesDiagram-AYQRV4AK-BlmdUBbW.js +0 -8
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
- 🔎 **Inspect code changes.** Open Codex’s saved patches for a turn or tool in the same read-only panel. Scroll through all files, inspect line numbers, see the exact words changed within each line, and jump between change blocks. Long diffs start collapsed and render when expanded. File headers stay visible while scrolling so you can collapse the current file.
|
|
40
40
|
- ✍️ **Switch tasks without losing your draft.** Each conversation keeps its own draft, pasted images, and model settings in the current page. Copy or export results when you're done.
|
|
41
41
|
- 🖥️ **Run a command beside your task.** Open **Shell** to execute commands in the selected project's directory, follow stdout/stderr, interrupt a run, and reuse recent commands. The panel uses Codex's configured sandbox, with a ten-minute limit and no interactive input. Closing it keeps the command running. [Capabilities and limits](docs/shell.md).
|
|
42
|
+
- 🛟 **Keep a recovery point.** Open **Backups and recovery** in the sidebar to create a server-side backup, follow its progress and review verified snapshots. Upgrades use the same backup engine. Recovery runs offline through the CLI. [Scope and recovery instructions](docs/backups.md).
|
|
42
43
|
|
|
43
44
|
Mermaid diagrams render locally after their code fence closes, with automatic light/dark colors. Failed diagrams keep their source; rendering is limited to 50,000 source characters and 500 edges.
|
|
44
45
|
|
|
@@ -150,6 +151,12 @@ Each release declares its recommended and verified Codex CLI versions. The updat
|
|
|
150
151
|
|
|
151
152
|
Automatic installation is off by default. Managed upgrades support **macOS and Linux npm global installations**; source, linked, container, and Windows installations use their original update method. A failed candidate is rolled back before business writes resume; downgrades must pass database compatibility checks. Old backups are retained without automatic rotation.
|
|
152
153
|
|
|
154
|
+
DB Schema **6** is tracked independently of the application version. Releases declare the schema and the verified backup migration range; an older application cannot open a newer database. See [database compatibility](docs/database.md).
|
|
155
|
+
|
|
156
|
+
Managed upgrades create and validate a private backup of the Covro database and service configuration before replacing the installation. A failed backup blocks activation. These snapshots do not include Codex history, credentials or project files. See [backup scope](docs/backups.md).
|
|
157
|
+
|
|
158
|
+
Use `covro backup create`, `covro backup list` and `covro backup verify ID` for manual snapshots. Preview offline recovery with `covro backup restore ID`; stop the service and repeat with `--confirm ID` to apply it. See [recovery instructions](docs/backups.md#manual-backups-and-offline-recovery).
|
|
159
|
+
|
|
153
160
|
See [distribution and recovery](docs/distribution.md) and [release operations](docs/releases.md) for details.
|
|
154
161
|
|
|
155
162
|
## Your computer, your workspace
|
package/README.zh-CN.md
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
- 🔎 **逐行查看代码变更。** 在同一只读预览窗口查看某轮或某工具的 Codex 补丁快照,按顺序连续浏览全部文件,支持行号、行内文字高亮和区块跳转;长 diff 默认折叠,展开后按需渲染,阅读时文件标题栏吸顶,随时收起当前文件。
|
|
40
40
|
- ✍️ **切换任务,草稿还在。** 每个会话分别保留当前页面中的草稿、粘贴图片和模型设置;完成后可以复制或导出结果。
|
|
41
41
|
- 🖥️ **在任务旁运行命令。** 打开 **Shell**,在所选项目目录执行命令、查看实时标准输出和错误输出、中断执行,并复用近期命令。面板沿用 Codex 配置的沙箱,最长运行十分钟,不支持交互输入;关闭面板后命令仍继续运行。[能力与限制](docs/shell.md)。
|
|
42
|
+
- 🛟 **保留可恢复的数据快照。** 从侧栏打开**备份与恢复**,手动创建服务端备份,查看进度和已验证的记录;升级前使用同一套备份能力。恢复通过停服后的 CLI 执行。[范围与恢复步骤](docs/backups.md)。
|
|
42
43
|
|
|
43
44
|
Mermaid 图表在代码块闭合后于本地渲染,自动适配浅色/深色。渲染失败时保留源码;渲染上限为 50,000 个源码字符、500 条边。
|
|
44
45
|
|
|
@@ -150,6 +151,12 @@ covro update --channel dev
|
|
|
150
151
|
|
|
151
152
|
自动安装默认关闭。受管升级支持 **macOS 和 Linux 的 npm 全局安装**;源码、链接、容器和 Windows 安装沿用原更新方式。候选版本失败时,会在重新开放业务写入前回滚;降级需通过数据库兼容检查。旧备份会保留,暂不自动轮转。
|
|
152
153
|
|
|
154
|
+
DB Schema **6** 独立于应用版本管理。发行版声明 Schema 及经过验证的备份迁移范围;旧应用不能打开更新的数据库。详见[数据库兼容规则](docs/database.md)。
|
|
155
|
+
|
|
156
|
+
受管升级会在替换安装前创建并验证私有备份,覆盖 Covro 数据库与服务配置;备份失败会阻止激活。快照不包含 Codex 历史、凭据或项目文件。详见[备份范围](docs/backups.md)。
|
|
157
|
+
|
|
158
|
+
使用 `covro backup create`、`covro backup list` 和 `covro backup verify ID` 创建与检查手动备份。先运行 `covro backup restore ID` 预检;停服后加 `--confirm ID` 执行恢复。详见[恢复步骤](docs/backups.md#manual-backups-and-offline-recovery)。
|
|
159
|
+
|
|
153
160
|
详细说明见[分发与恢复](docs/distribution.md)、[发版操作](docs/releases.md)。
|
|
154
161
|
|
|
155
162
|
## 你的电脑,你的工作台
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { packageInfo, paths } from "../cli/paths.js";
|
|
3
|
+
import { databaseCompatibilitySchema } from "../shared/database.js";
|
|
4
|
+
import { readMigrations } from "../server/db/history.js";
|
|
5
|
+
import { runtimeMigrationsFolder } from "../server/db/migrate.js";
|
|
6
|
+
import { BackupStore, backupDirectory } from "./store.js";
|
|
7
|
+
function createBackupStore(config, dataDir, migrationsFolder = runtimeMigrationsFolder) {
|
|
8
|
+
return new BackupStore({
|
|
9
|
+
directory: backupDirectory(paths(config).home, dataDir),
|
|
10
|
+
config,
|
|
11
|
+
dataDir,
|
|
12
|
+
appVersion: packageInfo.version,
|
|
13
|
+
commit: packageInfo.covroCommit,
|
|
14
|
+
contract: databaseCompatibilitySchema.parse(packageInfo.covroDatabase),
|
|
15
|
+
migrations: readMigrations(migrationsFolder),
|
|
16
|
+
Sqlite: Database
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
createBackupStore
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/backups/factory.ts"],
|
|
4
|
+
"sourcesContent": ["import Database from 'better-sqlite3';\nimport { packageInfo, paths } from '../cli/paths.js';\nimport { databaseCompatibilitySchema } from '../shared/database.js';\nimport { readMigrations } from '../server/db/history.js';\nimport { runtimeMigrationsFolder } from '../server/db/migrate.js';\nimport { BackupStore, backupDirectory } from './store.js';\n\nexport function createBackupStore(\n config: string,\n dataDir: string,\n migrationsFolder = runtimeMigrationsFolder,\n) {\n return new BackupStore({\n directory: backupDirectory(paths(config).home, dataDir),\n config,\n dataDir,\n appVersion: packageInfo.version,\n commit: packageInfo.covroCommit,\n contract: databaseCompatibilitySchema.parse(packageInfo.covroDatabase),\n migrations: readMigrations(migrationsFolder),\n Sqlite: Database,\n });\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,cAAc;AACrB,SAAS,aAAa,aAAa;AACnC,SAAS,mCAAmC;AAC5C,SAAS,sBAAsB;AAC/B,SAAS,+BAA+B;AACxC,SAAS,aAAa,uBAAuB;AAEtC,SAAS,kBACd,QACA,SACA,mBAAmB,yBACnB;AACA,SAAO,IAAI,YAAY;AAAA,IACrB,WAAW,gBAAgB,MAAM,MAAM,EAAE,MAAM,OAAO;AAAA,IACtD;AAAA,IACA;AAAA,IACA,YAAY,YAAY;AAAA,IACxB,QAAQ,YAAY;AAAA,IACpB,UAAU,4BAA4B,MAAM,YAAY,aAAa;AAAA,IACrE,YAAY,eAAe,gBAAgB;AAAA,IAC3C,QAAQ;AAAA,EACV,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import lockfile from "proper-lockfile";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
async function acquireMaintenanceLock(dataDir) {
|
|
6
|
+
try {
|
|
7
|
+
const directory = join(dataDir, ".covro-maintenance");
|
|
8
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
9
|
+
return await lockfile.lock(directory, {
|
|
10
|
+
realpath: true,
|
|
11
|
+
stale: 1e4,
|
|
12
|
+
update: 2e3,
|
|
13
|
+
retries: 0
|
|
14
|
+
});
|
|
15
|
+
} catch {
|
|
16
|
+
throw new AppError("MAINTENANCE_BUSY", "\u5DF2\u6709\u5907\u4EFD\u3001\u5347\u7EA7\u6216\u6062\u590D\u6B63\u5728\u5904\u7406\uFF0C\u8BF7\u5B8C\u6210\u540E\u91CD\u8BD5\u3002", 409);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
acquireMaintenanceLock
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/backups/lock.ts"],
|
|
4
|
+
"sourcesContent": ["import { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport lockfile from 'proper-lockfile';\nimport { AppError } from '../shared/errors.js';\n\n/** Shared by live manual snapshots, update workers and offline restoration. */\nexport async function acquireMaintenanceLock(dataDir: string) {\n try {\n const directory = join(dataDir, '.covro-maintenance');\n await mkdir(directory, { recursive: true, mode: 0o700 });\n return await lockfile.lock(directory, {\n realpath: true,\n stale: 10_000,\n update: 2_000,\n retries: 0,\n });\n } catch {\n throw new AppError('MAINTENANCE_BUSY', '\u5DF2\u6709\u5907\u4EFD\u3001\u5347\u7EA7\u6216\u6062\u590D\u6B63\u5728\u5904\u7406\uFF0C\u8BF7\u5B8C\u6210\u540E\u91CD\u8BD5\u3002', 409);\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,OAAO,cAAc;AACrB,SAAS,gBAAgB;AAGzB,eAAsB,uBAAuB,SAAiB;AAC5D,MAAI;AACF,UAAM,YAAY,KAAK,SAAS,oBAAoB;AACpD,UAAM,MAAM,WAAW,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACvD,WAAO,MAAM,SAAS,KAAK,WAAW;AAAA,MACpC,UAAU;AAAA,MACV,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AAAA,EACH,QAAQ;AACN,UAAM,IAAI,SAAS,oBAAoB,wIAA0B,GAAG;AAAA,EACtE;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { setTimeout } from "node:timers/promises";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { acquireMaintenanceLock } from "./lock.js";
|
|
5
|
+
import { backupFailure } from "./store.js";
|
|
6
|
+
const backupRequestSchema = z.object({ clientOperationId: z.string().uuid() }).strict();
|
|
7
|
+
const ongoing = (record) => ["waiting", "copying", "verifying"].includes(record.phase);
|
|
8
|
+
class BackupManager {
|
|
9
|
+
constructor(store, service, options = {}) {
|
|
10
|
+
this.store = store;
|
|
11
|
+
this.service = service;
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
store;
|
|
15
|
+
service;
|
|
16
|
+
options;
|
|
17
|
+
active;
|
|
18
|
+
abort;
|
|
19
|
+
task;
|
|
20
|
+
closed = false;
|
|
21
|
+
async status() {
|
|
22
|
+
const records = (await this.store.list()).map(
|
|
23
|
+
(record) => this.active?.id === record.id ? this.active : ongoing(record) ? {
|
|
24
|
+
...record,
|
|
25
|
+
phase: "interrupted",
|
|
26
|
+
message: "\u5907\u4EFD\u8FDB\u7A0B\u5DF2\u4E2D\u65AD\uFF0C\u672A\u751F\u6210\u53EF\u6062\u590D\u5FEB\u7167\u3002"
|
|
27
|
+
} : record
|
|
28
|
+
);
|
|
29
|
+
return {
|
|
30
|
+
directory: this.store.options.directory,
|
|
31
|
+
schemaVersion: this.store.options.contract.schemaVersion,
|
|
32
|
+
records,
|
|
33
|
+
active: this.active,
|
|
34
|
+
supported: !this.options.unsupportedReason,
|
|
35
|
+
unsupportedReason: this.options.unsupportedReason
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async start(input) {
|
|
39
|
+
const { clientOperationId: id } = backupRequestSchema.parse(input);
|
|
40
|
+
if (this.closed) throw new AppError("BACKUP_UNAVAILABLE", "\u670D\u52A1\u6B63\u5728\u5173\u95ED\u3002", 503);
|
|
41
|
+
if (this.options.unsupportedReason)
|
|
42
|
+
throw new AppError("BACKUP_UNAVAILABLE", this.options.unsupportedReason, 409);
|
|
43
|
+
if (this.active?.id === id || (await this.store.list()).some((record2) => record2.id === id))
|
|
44
|
+
return this.status();
|
|
45
|
+
if (this.active) throw new AppError("BACKUP_BUSY", "\u5DF2\u6709\u5907\u4EFD\u6B63\u5728\u5904\u7406\u3002", 409);
|
|
46
|
+
if (this.service.isInMaintenance()) throw this.service.maintenanceError();
|
|
47
|
+
const release = await acquireMaintenanceLock(this.store.options.dataDir);
|
|
48
|
+
if (this.closed) {
|
|
49
|
+
await release();
|
|
50
|
+
throw new AppError("BACKUP_UNAVAILABLE", "\u670D\u52A1\u6B63\u5728\u5173\u95ED\u3002", 503);
|
|
51
|
+
}
|
|
52
|
+
const abort = new AbortController();
|
|
53
|
+
const record = {
|
|
54
|
+
id,
|
|
55
|
+
phase: "waiting",
|
|
56
|
+
trigger: "manual",
|
|
57
|
+
createdAt: Date.now(),
|
|
58
|
+
updatedAt: Date.now(),
|
|
59
|
+
path: this.store.path(id),
|
|
60
|
+
message: "\u7B49\u5F85\u4EFB\u52A1\u3001\u5BA1\u6279\u3001\u8F93\u5165\u53CA\u5F85\u6838\u5BF9\u64CD\u4F5C\u7ED3\u675F"
|
|
61
|
+
};
|
|
62
|
+
this.active = record;
|
|
63
|
+
this.abort = abort;
|
|
64
|
+
const preparation = this.store.record(record);
|
|
65
|
+
this.task = (async () => {
|
|
66
|
+
try {
|
|
67
|
+
await preparation;
|
|
68
|
+
} catch {
|
|
69
|
+
await release();
|
|
70
|
+
this.active = void 0;
|
|
71
|
+
this.abort = void 0;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await this.run(record, abort, release);
|
|
75
|
+
})();
|
|
76
|
+
try {
|
|
77
|
+
await preparation;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new AppError("BACKUP_FAILED", backupFailure(error), 500);
|
|
80
|
+
}
|
|
81
|
+
return this.status();
|
|
82
|
+
}
|
|
83
|
+
async run(record, abort, release) {
|
|
84
|
+
let gated = false;
|
|
85
|
+
try {
|
|
86
|
+
for (; ; ) {
|
|
87
|
+
abort.signal.throwIfAborted();
|
|
88
|
+
if (Date.now() - record.createdAt > (this.options.timeoutMs ?? 30 * 6e4))
|
|
89
|
+
throw new AppError("BACKUP_TIMEOUT", "\u7B49\u5F85\u5B89\u5168\u70B9\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", 409);
|
|
90
|
+
const result = this.service.prepareUpdate("backup");
|
|
91
|
+
if (result.ready) {
|
|
92
|
+
gated = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
if (record.message !== result.reason) {
|
|
96
|
+
record.message = result.reason ?? "\u7B49\u5F85\u4EFB\u52A1\u7ED3\u675F";
|
|
97
|
+
record.updatedAt = Date.now();
|
|
98
|
+
await this.store.record(record);
|
|
99
|
+
}
|
|
100
|
+
await setTimeout(this.options.pollMs ?? 250, void 0, { signal: abort.signal });
|
|
101
|
+
}
|
|
102
|
+
await this.store.create({
|
|
103
|
+
id: record.id,
|
|
104
|
+
trigger: "manual",
|
|
105
|
+
reserved: true,
|
|
106
|
+
signal: abort.signal,
|
|
107
|
+
progress: (next) => {
|
|
108
|
+
this.active = next;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (this.active?.phase !== "failed" && this.active?.phase !== "cancelled") {
|
|
113
|
+
record.phase = abort.signal.aborted ? "cancelled" : "failed";
|
|
114
|
+
record.message = abort.signal.aborted ? "\u5907\u4EFD\u5DF2\u53D6\u6D88\u3002" : error instanceof AppError ? error.message : backupFailure(error);
|
|
115
|
+
record.updatedAt = Date.now();
|
|
116
|
+
await this.store.record(record).catch(() => {
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
if (gated) this.service.setMaintenance(false);
|
|
121
|
+
await release();
|
|
122
|
+
this.active = void 0;
|
|
123
|
+
this.abort = void 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async cancel(id) {
|
|
127
|
+
if (this.active?.id === id) {
|
|
128
|
+
this.abort?.abort();
|
|
129
|
+
await this.task;
|
|
130
|
+
}
|
|
131
|
+
return this.status();
|
|
132
|
+
}
|
|
133
|
+
async verify(id) {
|
|
134
|
+
if (this.active && !ongoing(this.active)) await this.task;
|
|
135
|
+
if (this.active) throw new AppError("BACKUP_BUSY", "\u8BF7\u7B49\u5F85\u5F53\u524D\u5907\u4EFD\u5B8C\u6210\u3002", 409);
|
|
136
|
+
try {
|
|
137
|
+
await this.store.verify(id);
|
|
138
|
+
const record = (await this.store.list()).find((entry) => entry.id === id);
|
|
139
|
+
if (record)
|
|
140
|
+
await this.store.record({
|
|
141
|
+
...record,
|
|
142
|
+
phase: "succeeded",
|
|
143
|
+
message: "\u5907\u4EFD\u5DF2\u91CD\u65B0\u9A8C\u8BC1\uFF0C\u5B8C\u6574\u6027\u68C0\u67E5\u901A\u8FC7\u3002",
|
|
144
|
+
updatedAt: Date.now()
|
|
145
|
+
});
|
|
146
|
+
} catch {
|
|
147
|
+
const record = (await this.store.list()).find((entry) => entry.id === id);
|
|
148
|
+
if (record)
|
|
149
|
+
await this.store.record({
|
|
150
|
+
...record,
|
|
151
|
+
phase: "corrupt",
|
|
152
|
+
message: "\u5907\u4EFD\u635F\u574F\u3001\u4E0D\u5B8C\u6574\u6216\u4E0E\u5F53\u524D\u7248\u672C\u4E0D\u517C\u5BB9\uFF0C\u4E0D\u80FD\u6062\u590D\u3002",
|
|
153
|
+
updatedAt: Date.now()
|
|
154
|
+
}).catch(() => {
|
|
155
|
+
});
|
|
156
|
+
throw new AppError("BACKUP_INVALID", "\u5907\u4EFD\u635F\u574F\u3001\u4E0D\u5B8C\u6574\u6216\u4E0E\u5F53\u524D\u7248\u672C\u4E0D\u517C\u5BB9\uFF0C\u4E0D\u80FD\u6062\u590D\u3002", 409);
|
|
157
|
+
}
|
|
158
|
+
return this.status();
|
|
159
|
+
}
|
|
160
|
+
async close() {
|
|
161
|
+
this.closed = true;
|
|
162
|
+
this.abort?.abort();
|
|
163
|
+
await this.task;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
BackupManager,
|
|
168
|
+
backupRequestSchema
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/backups/manager.ts"],
|
|
4
|
+
"sourcesContent": ["import { setTimeout } from 'node:timers/promises';\nimport { z } from 'zod';\nimport type { BackupController, BackupRecord, BackupStatus } from '../shared/backups.js';\nimport { AppError } from '../shared/errors.js';\nimport type { GatewayService } from '../server/service.js';\nimport { acquireMaintenanceLock } from './lock.js';\nimport { backupFailure, type BackupStore } from './store.js';\n\nexport const backupRequestSchema = z.object({ clientOperationId: z.string().uuid() }).strict();\nconst ongoing = (record: BackupRecord) =>\n ['waiting', 'copying', 'verifying'].includes(record.phase);\n\nexport class BackupManager implements BackupController {\n private active?: BackupRecord;\n private abort?: AbortController;\n private task?: Promise<void>;\n private closed = false;\n constructor(\n readonly store: BackupStore,\n private readonly service: GatewayService,\n private readonly options: {\n pollMs?: number;\n timeoutMs?: number;\n unsupportedReason?: string;\n } = {},\n ) {}\n async status(): Promise<BackupStatus> {\n const records = (await this.store.list()).map((record) =>\n this.active?.id === record.id\n ? this.active\n : ongoing(record)\n ? {\n ...record,\n phase: 'interrupted' as const,\n message: '\u5907\u4EFD\u8FDB\u7A0B\u5DF2\u4E2D\u65AD\uFF0C\u672A\u751F\u6210\u53EF\u6062\u590D\u5FEB\u7167\u3002',\n }\n : record,\n );\n return {\n directory: this.store.options.directory,\n schemaVersion: this.store.options.contract.schemaVersion,\n records,\n active: this.active,\n supported: !this.options.unsupportedReason,\n unsupportedReason: this.options.unsupportedReason,\n };\n }\n async start(input: { clientOperationId: string }) {\n const { clientOperationId: id } = backupRequestSchema.parse(input);\n if (this.closed) throw new AppError('BACKUP_UNAVAILABLE', '\u670D\u52A1\u6B63\u5728\u5173\u95ED\u3002', 503);\n if (this.options.unsupportedReason)\n throw new AppError('BACKUP_UNAVAILABLE', this.options.unsupportedReason, 409);\n if (this.active?.id === id || (await this.store.list()).some((record) => record.id === id))\n return this.status();\n if (this.active) throw new AppError('BACKUP_BUSY', '\u5DF2\u6709\u5907\u4EFD\u6B63\u5728\u5904\u7406\u3002', 409);\n if (this.service.isInMaintenance()) throw this.service.maintenanceError();\n const release = await acquireMaintenanceLock(this.store.options.dataDir);\n if (this.closed) {\n await release();\n throw new AppError('BACKUP_UNAVAILABLE', '\u670D\u52A1\u6B63\u5728\u5173\u95ED\u3002', 503);\n }\n const abort = new AbortController();\n const record: BackupRecord = {\n id,\n phase: 'waiting',\n trigger: 'manual',\n createdAt: Date.now(),\n updatedAt: Date.now(),\n path: this.store.path(id),\n message: '\u7B49\u5F85\u4EFB\u52A1\u3001\u5BA1\u6279\u3001\u8F93\u5165\u53CA\u5F85\u6838\u5BF9\u64CD\u4F5C\u7ED3\u675F',\n };\n this.active = record;\n this.abort = abort;\n const preparation = this.store.record(record);\n this.task = (async () => {\n try {\n await preparation;\n } catch {\n await release();\n this.active = undefined;\n this.abort = undefined;\n return;\n }\n await this.run(record, abort, release);\n })();\n try {\n await preparation;\n } catch (error) {\n throw new AppError('BACKUP_FAILED', backupFailure(error), 500);\n }\n return this.status();\n }\n private async run(record: BackupRecord, abort: AbortController, release: () => Promise<void>) {\n let gated = false;\n try {\n for (;;) {\n abort.signal.throwIfAborted();\n if (Date.now() - record.createdAt > (this.options.timeoutMs ?? 30 * 60_000))\n throw new AppError('BACKUP_TIMEOUT', '\u7B49\u5F85\u5B89\u5168\u70B9\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002', 409);\n const result = this.service.prepareUpdate('backup');\n if (result.ready) {\n gated = true;\n break;\n }\n if (record.message !== result.reason) {\n record.message = result.reason ?? '\u7B49\u5F85\u4EFB\u52A1\u7ED3\u675F';\n record.updatedAt = Date.now();\n await this.store.record(record);\n }\n await setTimeout(this.options.pollMs ?? 250, undefined, { signal: abort.signal });\n }\n await this.store.create({\n id: record.id,\n trigger: 'manual',\n reserved: true,\n signal: abort.signal,\n progress: (next) => {\n this.active = next;\n },\n });\n } catch (error) {\n if (this.active?.phase !== 'failed' && this.active?.phase !== 'cancelled') {\n record.phase = abort.signal.aborted ? 'cancelled' : 'failed';\n record.message = abort.signal.aborted\n ? '\u5907\u4EFD\u5DF2\u53D6\u6D88\u3002'\n : error instanceof AppError\n ? error.message\n : backupFailure(error);\n record.updatedAt = Date.now();\n await this.store.record(record).catch(() => {});\n }\n } finally {\n if (gated) this.service.setMaintenance(false);\n await release();\n this.active = undefined;\n this.abort = undefined;\n }\n }\n async cancel(id: string) {\n if (this.active?.id === id) {\n this.abort?.abort();\n await this.task;\n }\n return this.status();\n }\n async verify(id: string) {\n if (this.active && !ongoing(this.active)) await this.task;\n if (this.active) throw new AppError('BACKUP_BUSY', '\u8BF7\u7B49\u5F85\u5F53\u524D\u5907\u4EFD\u5B8C\u6210\u3002', 409);\n try {\n await this.store.verify(id);\n const record = (await this.store.list()).find((entry) => entry.id === id);\n if (record)\n await this.store.record({\n ...record,\n phase: 'succeeded',\n message: '\u5907\u4EFD\u5DF2\u91CD\u65B0\u9A8C\u8BC1\uFF0C\u5B8C\u6574\u6027\u68C0\u67E5\u901A\u8FC7\u3002',\n updatedAt: Date.now(),\n });\n } catch {\n const record = (await this.store.list()).find((entry) => entry.id === id);\n if (record)\n await this.store\n .record({\n ...record,\n phase: 'corrupt',\n message: '\u5907\u4EFD\u635F\u574F\u3001\u4E0D\u5B8C\u6574\u6216\u4E0E\u5F53\u524D\u7248\u672C\u4E0D\u517C\u5BB9\uFF0C\u4E0D\u80FD\u6062\u590D\u3002',\n updatedAt: Date.now(),\n })\n .catch(() => {});\n throw new AppError('BACKUP_INVALID', '\u5907\u4EFD\u635F\u574F\u3001\u4E0D\u5B8C\u6574\u6216\u4E0E\u5F53\u524D\u7248\u672C\u4E0D\u517C\u5BB9\uFF0C\u4E0D\u80FD\u6062\u590D\u3002', 409);\n }\n return this.status();\n }\n async close() {\n this.closed = true;\n this.abort?.abort();\n await this.task;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["record"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { chmod, lstat, mkdir, readFile, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { acquireInstanceLock } from "../server/instance-lock.js";
|
|
8
|
+
import { parseConfig } from "../server/config.js";
|
|
9
|
+
import { migrateSqlite } from "../server/db/history.js";
|
|
10
|
+
import { copyDurable, readJson, saveJson, syncDirectory } from "../updates/files.js";
|
|
11
|
+
import { acquireMaintenanceLock } from "./lock.js";
|
|
12
|
+
const fileNames = ["app.sqlite", "app.sqlite-wal", "app.sqlite-shm", "config.json"];
|
|
13
|
+
const journalSchema = z.object({
|
|
14
|
+
id: z.string().uuid(),
|
|
15
|
+
backupId: z.string(),
|
|
16
|
+
phase: z.enum(["prepared", "replacing", "committed", "rolledBack"]),
|
|
17
|
+
originals: z.array(
|
|
18
|
+
z.object({ name: z.enum(fileNames), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict()
|
|
19
|
+
).max(4)
|
|
20
|
+
}).strict();
|
|
21
|
+
const journalPath = (store) => join(store.options.directory, "restore.json");
|
|
22
|
+
const stagePath = (store, id) => join(store.options.dataDir, `.covro-restore-${id}`);
|
|
23
|
+
const configStage = (store, id) => join(dirname(store.options.config), `.covro-restore-${id}.tmp`);
|
|
24
|
+
async function hash(path) {
|
|
25
|
+
const info = await lstat(path);
|
|
26
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
27
|
+
throw new Error("Recovery file is not a regular file");
|
|
28
|
+
const digest = createHash("sha256");
|
|
29
|
+
for await (const chunk of createReadStream(path)) digest.update(chunk);
|
|
30
|
+
return digest.digest("hex");
|
|
31
|
+
}
|
|
32
|
+
async function assertNoPendingRestore(directory) {
|
|
33
|
+
const value = await readJson(join(directory, "restore.json"));
|
|
34
|
+
if (value === void 0) return;
|
|
35
|
+
const journal = journalSchema.parse(value);
|
|
36
|
+
if (["prepared", "replacing"].includes(journal.phase))
|
|
37
|
+
throw new AppError(
|
|
38
|
+
"RESTORE_PENDING",
|
|
39
|
+
"\u4E0A\u6B21\u6062\u590D\u5C1A\u672A\u5B8C\u6210\uFF0C\u62D2\u7EDD\u5F00\u653E\u5199\u5165\uFF1B\u8BF7\u8FD0\u884C covro backup repair\u3002",
|
|
40
|
+
503
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
async function restoreOriginals(store, journal) {
|
|
44
|
+
const directory = stagePath(store, journal.id);
|
|
45
|
+
for (const file of journal.originals)
|
|
46
|
+
if (await hash(join(directory, "original", file.name)) !== file.sha256)
|
|
47
|
+
throw new Error("Recovery copy checksum mismatch");
|
|
48
|
+
for (const name of fileNames) {
|
|
49
|
+
const target = name === "config.json" ? store.options.config : join(store.options.dataDir, name);
|
|
50
|
+
const original = journal.originals.find((file) => file.name === name);
|
|
51
|
+
if (original) {
|
|
52
|
+
await copyDurable(join(directory, "original", name), target);
|
|
53
|
+
await chmod(target, 384);
|
|
54
|
+
} else await rm(target, { force: true });
|
|
55
|
+
}
|
|
56
|
+
await rm(configStage(store, journal.id), { force: true });
|
|
57
|
+
await saveJson(journalPath(store), { ...journal, phase: "rolledBack" });
|
|
58
|
+
}
|
|
59
|
+
async function previewRestore(store, id) {
|
|
60
|
+
const manifest = await store.verify(id);
|
|
61
|
+
if (resolve(manifest.configPath) !== resolve(store.options.config) || resolve(manifest.dataDir) !== resolve(store.options.dataDir))
|
|
62
|
+
throw new AppError(
|
|
63
|
+
"RESTORE_INSTANCE_MISMATCH",
|
|
64
|
+
"\u6B64\u5907\u4EFD\u5C5E\u4E8E\u5176\u4ED6\u914D\u7F6E\u6216\u6570\u636E\u76EE\u5F55\uFF0C\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u5B9E\u4F8B\u3002",
|
|
65
|
+
409
|
|
66
|
+
);
|
|
67
|
+
const config = parseConfig(
|
|
68
|
+
JSON.parse(await readFile(join(store.path(id), "config.json"), "utf8")),
|
|
69
|
+
store.options.config
|
|
70
|
+
);
|
|
71
|
+
if (config.dataDir !== resolve(store.options.dataDir))
|
|
72
|
+
throw new AppError("RESTORE_CONFIG_MISMATCH", "\u5907\u4EFD\u914D\u7F6E\u4F1A\u5207\u6362\u6570\u636E\u76EE\u5F55\uFF0C\u62D2\u7EDD\u6062\u590D\u3002", 409);
|
|
73
|
+
return {
|
|
74
|
+
backupId: id,
|
|
75
|
+
appVersion: manifest.appVersion,
|
|
76
|
+
backupSchema: manifest.schemaVersion,
|
|
77
|
+
targetSchema: store.options.contract.schemaVersion,
|
|
78
|
+
overwrite: [store.options.config, join(store.options.dataDir, "app.sqlite")],
|
|
79
|
+
warning: "\u5C06\u8986\u76D6\u670D\u52A1\u914D\u7F6E\u53CA Covro \u5E94\u7528\u6570\u636E\uFF0C\u4E22\u5F03\u5907\u4EFD\u4E4B\u540E\u7684\u5E94\u7528\u6570\u636E\uFF1BCodex \u5386\u53F2\u4E0E\u9879\u76EE\u6587\u4EF6\u4E0D\u53C2\u4E0E\u6062\u590D\u3002",
|
|
80
|
+
confirmation: `covro backup restore ${id} --confirm ${id} --config ${JSON.stringify(store.options.config)}`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function restoreBackup(store, id, confirm) {
|
|
84
|
+
const preview = await previewRestore(store, id);
|
|
85
|
+
if (confirm === void 0) return { ...preview, restored: false };
|
|
86
|
+
if (confirm !== id)
|
|
87
|
+
throw new AppError("RESTORE_CONFIRMATION", "\u786E\u8BA4\u503C\u5FC5\u987B\u4E0E\u5F85\u6062\u590D\u7684\u5907\u4EFD ID \u5B8C\u5168\u4E00\u81F4\u3002", 400);
|
|
88
|
+
const releaseInstance = await acquireInstanceLock(store.options.dataDir);
|
|
89
|
+
let releaseMaintenance;
|
|
90
|
+
let journal;
|
|
91
|
+
try {
|
|
92
|
+
releaseMaintenance = await acquireMaintenanceLock(store.options.dataDir);
|
|
93
|
+
await assertNoPendingRestore(store.options.directory);
|
|
94
|
+
await previewRestore(store, id);
|
|
95
|
+
const operation = randomUUID();
|
|
96
|
+
const directory = stagePath(store, operation);
|
|
97
|
+
await mkdir(join(directory, "original"), { recursive: true, mode: 448 });
|
|
98
|
+
const originals = [];
|
|
99
|
+
for (const name of fileNames) {
|
|
100
|
+
const source = name === "config.json" ? store.options.config : join(store.options.dataDir, name);
|
|
101
|
+
try {
|
|
102
|
+
await hash(source);
|
|
103
|
+
await copyDurable(source, join(directory, "original", name));
|
|
104
|
+
await chmod(join(directory, "original", name), 384);
|
|
105
|
+
originals.push({ name, sha256: await hash(join(directory, "original", name)) });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error.code !== "ENOENT") throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await copyDurable(join(store.path(id), "app.sqlite"), join(directory, "app.sqlite"));
|
|
111
|
+
const staged = new store.options.Sqlite(join(directory, "app.sqlite"));
|
|
112
|
+
try {
|
|
113
|
+
migrateSqlite(staged, store.options.migrations);
|
|
114
|
+
staged.pragma("journal_mode = DELETE");
|
|
115
|
+
if (staged.pragma("integrity_check", { simple: true }) !== "ok" || staged.pragma("foreign_key_check").length)
|
|
116
|
+
throw new Error("Staged migration failed integrity check");
|
|
117
|
+
} finally {
|
|
118
|
+
staged.close();
|
|
119
|
+
}
|
|
120
|
+
await chmod(join(directory, "app.sqlite"), 384);
|
|
121
|
+
await copyDurable(join(store.path(id), "config.json"), configStage(store, operation));
|
|
122
|
+
await chmod(configStage(store, operation), 384);
|
|
123
|
+
journal = { id: operation, backupId: id, phase: "prepared", originals };
|
|
124
|
+
await saveJson(journalPath(store), journal);
|
|
125
|
+
journal.phase = "replacing";
|
|
126
|
+
await saveJson(journalPath(store), journal);
|
|
127
|
+
for (const name of ["app.sqlite-wal", "app.sqlite-shm"])
|
|
128
|
+
await rm(join(store.options.dataDir, name), { force: true });
|
|
129
|
+
await rename(join(directory, "app.sqlite"), join(store.options.dataDir, "app.sqlite"));
|
|
130
|
+
await rename(configStage(store, operation), store.options.config);
|
|
131
|
+
await syncDirectory(store.options.dataDir);
|
|
132
|
+
await syncDirectory(dirname(store.options.config));
|
|
133
|
+
await saveJson(journalPath(store), { ...journal, phase: "committed" });
|
|
134
|
+
return {
|
|
135
|
+
...preview,
|
|
136
|
+
restored: true,
|
|
137
|
+
recoveryCopy: directory,
|
|
138
|
+
message: "\u6062\u590D\u5B8C\u6210\uFF0C\u670D\u52A1\u4FDD\u6301\u505C\u6B62\uFF1B\u8FD0\u884C covro start \u542F\u52A8\u3002"
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (journal) {
|
|
142
|
+
try {
|
|
143
|
+
await restoreOriginals(store, journal);
|
|
144
|
+
} catch {
|
|
145
|
+
throw new AppError(
|
|
146
|
+
"RESTORE_PENDING",
|
|
147
|
+
"\u6062\u590D\u5931\u8D25\u4E14\u539F\u6570\u636E\u5C1A\u672A\u6062\u590D\uFF0C\u5DF2\u4FDD\u7559\u73B0\u573A\uFF1B\u8BF7\u8FD0\u884C covro backup repair\u3002",
|
|
148
|
+
500
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!journal && error instanceof AppError) throw error;
|
|
153
|
+
throw new AppError(
|
|
154
|
+
"RESTORE_FAILED",
|
|
155
|
+
"\u6062\u590D\u672A\u5B8C\u6210\uFF0C\u539F\u6570\u636E\u5DF2\u4FDD\u7559\uFF1B\u8BF7\u68C0\u67E5\u78C1\u76D8\u3001\u6743\u9650\u53CA\u5907\u4EFD\u540E\u91CD\u8BD5\u3002",
|
|
156
|
+
500
|
|
157
|
+
);
|
|
158
|
+
} finally {
|
|
159
|
+
await releaseMaintenance?.();
|
|
160
|
+
await releaseInstance();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function repairRestore(store) {
|
|
164
|
+
const releaseInstance = await acquireInstanceLock(store.options.dataDir);
|
|
165
|
+
let releaseMaintenance;
|
|
166
|
+
try {
|
|
167
|
+
releaseMaintenance = await acquireMaintenanceLock(store.options.dataDir);
|
|
168
|
+
const journal = journalSchema.parse(await readJson(journalPath(store)));
|
|
169
|
+
if (["committed", "rolledBack"].includes(journal.phase))
|
|
170
|
+
return { repaired: false, phase: journal.phase };
|
|
171
|
+
await restoreOriginals(store, journal);
|
|
172
|
+
return {
|
|
173
|
+
repaired: true,
|
|
174
|
+
phase: "rolledBack",
|
|
175
|
+
message: "\u5DF2\u6062\u590D\u6062\u590D\u64CD\u4F5C\u524D\u7684\u6587\u4EF6\uFF0C\u670D\u52A1\u4FDD\u6301\u505C\u6B62\u3002"
|
|
176
|
+
};
|
|
177
|
+
} finally {
|
|
178
|
+
await releaseMaintenance?.();
|
|
179
|
+
await releaseInstance();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
assertNoPendingRestore,
|
|
184
|
+
previewRestore,
|
|
185
|
+
repairRestore,
|
|
186
|
+
restoreBackup
|
|
187
|
+
};
|
|
188
|
+
//# sourceMappingURL=restore.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/backups/restore.ts"],
|
|
4
|
+
"sourcesContent": ["import { randomUUID, createHash } from 'node:crypto';\nimport { createReadStream } from 'node:fs';\nimport { chmod, lstat, mkdir, readFile, rename, rm } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { z } from 'zod';\nimport { AppError } from '../shared/errors.js';\nimport { acquireInstanceLock } from '../server/instance-lock.js';\nimport { parseConfig } from '../server/config.js';\nimport { migrateSqlite } from '../server/db/history.js';\nimport { copyDurable, readJson, saveJson, syncDirectory } from '../updates/files.js';\nimport { acquireMaintenanceLock } from './lock.js';\nimport { type BackupStore } from './store.js';\n\nconst fileNames = ['app.sqlite', 'app.sqlite-wal', 'app.sqlite-shm', 'config.json'] as const;\nconst journalSchema = z\n .object({\n id: z.string().uuid(),\n backupId: z.string(),\n phase: z.enum(['prepared', 'replacing', 'committed', 'rolledBack']),\n originals: z\n .array(\n z.object({ name: z.enum(fileNames), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict(),\n )\n .max(4),\n })\n .strict();\ntype Journal = z.infer<typeof journalSchema>;\nconst journalPath = (store: BackupStore) => join(store.options.directory, 'restore.json');\nconst stagePath = (store: BackupStore, id: string) =>\n join(store.options.dataDir, `.covro-restore-${id}`);\nconst configStage = (store: BackupStore, id: string) =>\n join(dirname(store.options.config), `.covro-restore-${id}.tmp`);\nasync function hash(path: string) {\n const info = await lstat(path);\n if (!info.isFile() || info.isSymbolicLink())\n throw new Error('Recovery file is not a regular file');\n const digest = createHash('sha256');\n for await (const chunk of createReadStream(path)) digest.update(chunk);\n return digest.digest('hex');\n}\nexport async function assertNoPendingRestore(directory: string) {\n const value = await readJson(join(directory, 'restore.json'));\n if (value === undefined) return;\n const journal = journalSchema.parse(value);\n if (['prepared', 'replacing'].includes(journal.phase))\n throw new AppError(\n 'RESTORE_PENDING',\n '\u4E0A\u6B21\u6062\u590D\u5C1A\u672A\u5B8C\u6210\uFF0C\u62D2\u7EDD\u5F00\u653E\u5199\u5165\uFF1B\u8BF7\u8FD0\u884C covro backup repair\u3002',\n 503,\n );\n}\nasync function restoreOriginals(store: BackupStore, journal: Journal) {\n const directory = stagePath(store, journal.id);\n for (const file of journal.originals)\n if ((await hash(join(directory, 'original', file.name))) !== file.sha256)\n throw new Error('Recovery copy checksum mismatch');\n for (const name of fileNames) {\n const target =\n name === 'config.json' ? store.options.config : join(store.options.dataDir, name);\n const original = journal.originals.find((file) => file.name === name);\n if (original) {\n await copyDurable(join(directory, 'original', name), target);\n await chmod(target, 0o600);\n } else await rm(target, { force: true });\n }\n await rm(configStage(store, journal.id), { force: true });\n await saveJson(journalPath(store), { ...journal, phase: 'rolledBack' });\n}\nexport async function previewRestore(store: BackupStore, id: string) {\n const manifest = await store.verify(id);\n if (\n resolve(manifest.configPath) !== resolve(store.options.config) ||\n resolve(manifest.dataDir) !== resolve(store.options.dataDir)\n )\n throw new AppError(\n 'RESTORE_INSTANCE_MISMATCH',\n '\u6B64\u5907\u4EFD\u5C5E\u4E8E\u5176\u4ED6\u914D\u7F6E\u6216\u6570\u636E\u76EE\u5F55\uFF0C\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u5B9E\u4F8B\u3002',\n 409,\n );\n const config = parseConfig(\n JSON.parse(await readFile(join(store.path(id), 'config.json'), 'utf8')),\n store.options.config,\n );\n if (config.dataDir !== resolve(store.options.dataDir))\n throw new AppError('RESTORE_CONFIG_MISMATCH', '\u5907\u4EFD\u914D\u7F6E\u4F1A\u5207\u6362\u6570\u636E\u76EE\u5F55\uFF0C\u62D2\u7EDD\u6062\u590D\u3002', 409);\n return {\n backupId: id,\n appVersion: manifest.appVersion,\n backupSchema: manifest.schemaVersion,\n targetSchema: store.options.contract.schemaVersion,\n overwrite: [store.options.config, join(store.options.dataDir, 'app.sqlite')],\n warning:\n '\u5C06\u8986\u76D6\u670D\u52A1\u914D\u7F6E\u53CA Covro \u5E94\u7528\u6570\u636E\uFF0C\u4E22\u5F03\u5907\u4EFD\u4E4B\u540E\u7684\u5E94\u7528\u6570\u636E\uFF1BCodex \u5386\u53F2\u4E0E\u9879\u76EE\u6587\u4EF6\u4E0D\u53C2\u4E0E\u6062\u590D\u3002',\n confirmation: `covro backup restore ${id} --confirm ${id} --config ${JSON.stringify(store.options.config)}`,\n };\n}\n/** Offline only: gateway startup, explicit migrations and restore all share the instance lock. */\nexport async function restoreBackup(store: BackupStore, id: string, confirm?: string) {\n const preview = await previewRestore(store, id);\n if (confirm === undefined) return { ...preview, restored: false as const };\n if (confirm !== id)\n throw new AppError('RESTORE_CONFIRMATION', '\u786E\u8BA4\u503C\u5FC5\u987B\u4E0E\u5F85\u6062\u590D\u7684\u5907\u4EFD ID \u5B8C\u5168\u4E00\u81F4\u3002', 400);\n const releaseInstance = await acquireInstanceLock(store.options.dataDir);\n let releaseMaintenance: (() => Promise<void>) | undefined;\n let journal: Journal | undefined;\n try {\n releaseMaintenance = await acquireMaintenanceLock(store.options.dataDir);\n await assertNoPendingRestore(store.options.directory);\n await previewRestore(store, id); // Revalidate after obtaining exclusive ownership.\n const operation = randomUUID();\n const directory = stagePath(store, operation);\n await mkdir(join(directory, 'original'), { recursive: true, mode: 0o700 });\n const originals: Journal['originals'] = [];\n for (const name of fileNames) {\n const source =\n name === 'config.json' ? store.options.config : join(store.options.dataDir, name);\n try {\n await hash(source);\n await copyDurable(source, join(directory, 'original', name));\n await chmod(join(directory, 'original', name), 0o600);\n originals.push({ name, sha256: await hash(join(directory, 'original', name)) });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n }\n await copyDurable(join(store.path(id), 'app.sqlite'), join(directory, 'app.sqlite'));\n const staged = new store.options.Sqlite(join(directory, 'app.sqlite'));\n try {\n migrateSqlite(staged, store.options.migrations);\n staged.pragma('journal_mode = DELETE');\n if (\n staged.pragma('integrity_check', { simple: true }) !== 'ok' ||\n (staged.pragma('foreign_key_check') as unknown[]).length\n )\n throw new Error('Staged migration failed integrity check');\n } finally {\n staged.close();\n }\n await chmod(join(directory, 'app.sqlite'), 0o600);\n await copyDurable(join(store.path(id), 'config.json'), configStage(store, operation));\n await chmod(configStage(store, operation), 0o600);\n journal = { id: operation, backupId: id, phase: 'prepared', originals };\n await saveJson(journalPath(store), journal);\n journal.phase = 'replacing';\n await saveJson(journalPath(store), journal);\n for (const name of ['app.sqlite-wal', 'app.sqlite-shm'])\n await rm(join(store.options.dataDir, name), { force: true });\n await rename(join(directory, 'app.sqlite'), join(store.options.dataDir, 'app.sqlite'));\n await rename(configStage(store, operation), store.options.config);\n await syncDirectory(store.options.dataDir);\n await syncDirectory(dirname(store.options.config));\n await saveJson(journalPath(store), { ...journal, phase: 'committed' });\n return {\n ...preview,\n restored: true as const,\n recoveryCopy: directory,\n message: '\u6062\u590D\u5B8C\u6210\uFF0C\u670D\u52A1\u4FDD\u6301\u505C\u6B62\uFF1B\u8FD0\u884C covro start \u542F\u52A8\u3002',\n };\n } catch (error) {\n if (journal) {\n try {\n await restoreOriginals(store, journal);\n } catch {\n throw new AppError(\n 'RESTORE_PENDING',\n '\u6062\u590D\u5931\u8D25\u4E14\u539F\u6570\u636E\u5C1A\u672A\u6062\u590D\uFF0C\u5DF2\u4FDD\u7559\u73B0\u573A\uFF1B\u8BF7\u8FD0\u884C covro backup repair\u3002',\n 500,\n );\n }\n }\n if (!journal && error instanceof AppError) throw error;\n throw new AppError(\n 'RESTORE_FAILED',\n '\u6062\u590D\u672A\u5B8C\u6210\uFF0C\u539F\u6570\u636E\u5DF2\u4FDD\u7559\uFF1B\u8BF7\u68C0\u67E5\u78C1\u76D8\u3001\u6743\u9650\u53CA\u5907\u4EFD\u540E\u91CD\u8BD5\u3002',\n 500,\n );\n } finally {\n await releaseMaintenance?.();\n await releaseInstance();\n }\n}\nexport async function repairRestore(store: BackupStore) {\n const releaseInstance = await acquireInstanceLock(store.options.dataDir);\n let releaseMaintenance: (() => Promise<void>) | undefined;\n try {\n releaseMaintenance = await acquireMaintenanceLock(store.options.dataDir);\n const journal = journalSchema.parse(await readJson(journalPath(store)));\n if (['committed', 'rolledBack'].includes(journal.phase))\n return { repaired: false, phase: journal.phase };\n await restoreOriginals(store, journal);\n return {\n repaired: true,\n phase: 'rolledBack',\n message: '\u5DF2\u6062\u590D\u6062\u590D\u64CD\u4F5C\u524D\u7684\u6587\u4EF6\uFF0C\u670D\u52A1\u4FDD\u6301\u505C\u6B62\u3002',\n };\n } finally {\n await releaseMaintenance?.();\n await releaseInstance();\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|