Files
blog/scripts/backup-bundle.mjs
T
zqlit 7abef4ac13 docs: 全量梳理文档结构;补 Gitea 迁移指南 / 架构精简候选;架构总览补证书管家子系统
用户四问:「把项目文件全部理一遍 还有readme文件」「gitea 后面可能还要迁移 帮我写一个迁移文档」
「证书管家做完了吗」「感觉现在架构还是复杂了」

一、文档梳理:根目录 15 个 md → 2 个

根目录只留 README.md(入口)+ 架构总览.md(现状唯一事实源),其余全部归位:

  docs/
  ├── README.md              ← 新增:文档地图(入口 / 当前有效 / 历史归档 三层)
  ├── 证书管家.md             ← 原「函数版证书管家-方案.md」改名
  │                              (它早已是「现状+沿革」文档,标题名不副实 —— 签发早已不在 CF Worker)
  ├── Gitea迁移指南.md        ← 新增
  ├── 架构精简候选.md         ← 新增
  ├── CNB构建落地方案.md / GITEA_SECRETS.md
  └── archive/{平台与选型,功能与修复,主题与内容}/   ← 32 份

- 32 个归档文档统一加 `> 📦 本文档已归档` banner,并指向架构总览
  —— 这个仓库历史文档里全是「权威口径」,混看很容易拿废弃结论当现状
- gitea-backup/ → deploy/gitea/(原名「backup」不准,它是部署包;与其它 deploy 单元并列)
- 新增 md 链接校验(python 脚本,中文路径用 sed 不安全)→ 首轮 18 处断链
  (多一层目录要让 banner 里的相对路径补一个 ../)→ 修正后 0 断链

二、新增 docs/Gitea迁移指南.md(★ 有死线:境外 VPS 11 月到期)

★ 核心价值是「指出迁移面已大幅缩小」:仓库原有的 deploy/gitea/迁移前检查清单.md
(2026-10-04)是为「Gitea + act_runner + 又拍云同步 *整套* 搬迁」写的,**那个前提已经不存在**——
构建归 CNB,runner / upyun-sync / 中转机 / COS 都不需要了。迁移实际只剩「搬数据卷 + 改地址」。

★★ 全文最重要:本仓有 9 处写死了旧地址,按易漏程度排序(1-3 本机,4-5 线上)
  1 本机 .git/config 的 gitea remote
  2 scripts/setup-cnb-remotes.sh 的 GITEA_URL 默认值
  3 deploy/editor-api/bootstrap.sh 的 GITEA_URL 默认值
  4 ★ /srv/editor-api/.env 的 GITEA_URL      ← 漏了会「持续报错但只警告不阻断」,没人发现
  5 ★ /srv/blog 的 gitea remote               ← 同上
  6 docs/GITEA_SECRETS.md
  7-9 架构总览.md / README.md / editor-api/README.md
(不用改:docker-compose.editor.yml、server.mjs、pushall 别名 —— 只引用 remote 名字)

另含:建议新实例改用域名而非 IP(以后搬机器只改 DNS;⚠️ Gitea 28 起只读 ROOT_URL 不读
[server] DOMAIN);rsync 而非 dump(属主必须 uid/gid 1000);切换顺序「先建新的→验证→再拆旧的」;
验证清单强调 `git push gitea --dry-run`;第九节给出「干脆不留这个辅仓」的选项与判断依据。

顺带修掉两个硬伤:
- deploy/gitea/docker-compose.yml 的镜像 tag `1.28.0-rootless` 根本不存在
  (Gitea 28 起去掉 1. 前缀)→ 改 28.0.0-rootless(pin 死,不用 latest)
- deploy/gitea/.gitignore 漏了 backups/(跑一次备份就会把含 secrets 的 dump 写进历史)

三、证书管家核实(结论:已上线运行,2 个遗留)

线上实测:容器 Up (healthy);/preflight 7/7 全过;3 组域名;下次自动续期 04:10。

遗留 ① writeapi.usj.cc 证书没纳管(真问题):nginx 配置在 /www/conf.d/writeapi.usj.cc.conf,
不在 /www/sites/ 管理树里 → 1Panel 的 ssl/upload+sslID 物化碰不到它。
实测仍是 RSA / CN=usj.cc / 到期 2026-12-07,本项目续期不会更新 → 12 月会断。
遗留 ② dnsapi.usj.cc 没上 HTTPS。
(澄清假问题:200181.xyz 公网看到的 LE 证书是 CF 自家边缘证书,与本项目无关)

四、新增 docs/架构精简候选.md(回应「架构还是复杂了」)

结论:复杂度不在件数,在「跨 6 个环境,其中 4 个要自己维护」。国内机 9 个容器里属于本项目的
只有 3 个,能动的只有 2 个。5 个候选 + 建议执行顺序:
  1 停 certimate 容器(工作流已全停用)——先 stop 观察,别急着删
  2 cn-dns-helper 大概率已是遗留(「Worker 侧签发」时代产物;现在签发在国内机且自带 dnsprovider,
    /preflight 显示 CF 凭据可读;Worker 侧 dnsremoted.ts 已无任何引用)
  3 Gitea:迁 or 不留
  4 两套写作前端收敛(本轮不动,先看使用频率)
  5 writeapi.usj.cc 证书纳管(12 月死线)
并明确列出 6 项「必要复杂度,不建议动」。

五、架构总览补齐证书管家子系统(★ 之前完全缺席)

一个完整子系统在「事实源文档」里一个字都没有 —— 这本身就是文档债。补:
- §0 一句话(四→五个子系统)、§1.1 子系统表、§1.2 地址地图两行
- **新增 §5.7 证书管家**:职责划分 / 为什么非要这么分(Worker 免费版 CPU 10ms)/
  三条不能破的红线(Worker 不得签发 · 证书路由只认 Bearer 会话 · 角色必须正着枚举放行)/
  纳管域名表 / 与 certimate 的关系 / 已知遗留
- .cnb.yml 行数 284 → 337(数字漂了);头部更新时间 → 2026-10-06;附录表改指 docs/
- §6 待办:#13(证书遗留)、#14(复杂度盘点)新增;#7 改写为「Gitea 11 月到期,★ 有死线」

六、记忆

.workbuddy/memory/MEMORY.md 新增「文档结构」「Gitea 迁移死线」两节;证书管家节补实测与遗留。
2026-10-06 22:27:36 +08:00

370 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 整仓备份 → 单个 bundle 文件 → AES-256-GCM 加密 → WebDAV 上传到 OpenList。
*
* 为什么是 bundle 而不是直接推 git:
* WebDAV 不支持原子的 rename/lock,把 bare repo 挂上去直接 `git push` 会让对象写坏
* (表面成功、实际随机损坏,可能几个月后才发现)。bundle 是单文件顺序写,安全。
*
* 为什么默认加密:
* 本仓库历史里含 .env、TLS 私钥、docs/GITEA_SECRETS.md。目标介质是手机内部存储,
* 未加密等于把密钥明文放在一台可能被刷机/丢失/他人访问的设备上。
*
* 为什么不用 gpg:
* 本机 gpg 2.4.9 在 Windows 下已损坏(反复 `removing stale lockfile`,node spawn 直接 EBUSY)。
* 改用 Node 内置 crypto 的 AES-256-GCM:零外部依赖、带认证标签(能检测篡改/截断)、
* 可流式处理 600 MB 不爆内存。加密格式见下方注释,解密由本脚本 `--decrypt` 完成。
*
* ★ 打包前先 `git fetch --all`(2026-10-06 补):
* `git bundle create --all` 取的是**本地已知**的 refs —— 其中
* `refs/remotes/origin/main` 停在上一次 fetch/pull 的位置。
* 而写作后台(editor-api)发的文章只落在 CNB 主仓,本机这份工作区
* 并不会自动跟着更新;不 fetch 就打 bundle,等于把**过期的快照**当成备份,
* 后台最近发的文章一份都不在里面 —— 而且失败是静默的(备份照样"成功")。
* 所以这里先 fetch,再打包。fetch 失败**不致命**(离线也得出得来备份),
* 只降级为「用本地已有 ref 打包」并显著告警。
*
* 用法:
* node scripts/backup-bundle.mjs # fetch + 加密 + 上传 + 比对字节数
* node scripts/backup-bundle.mjs --verify # 额外下载回来比对 sha256(慢,但端到端最可靠)
* node scripts/backup-bundle.mjs --dry # 只打包加密,不上传
* node scripts/backup-bundle.mjs --keep 7 # 远端保留最近 7 份(默认)
* node scripts/backup-bundle.mjs --no-fetch # 跳过 fetch(离线/调试用)
* node scripts/backup-bundle.mjs --no-encrypt # 不加密(仅当历史里的敏感文件已洗净)
* node scripts/backup-bundle.mjs --decrypt <文件> [--out x.bundle] # 解密(恢复用)
* node scripts/backup-bundle.mjs --list # 列出远端现有备份
*
* 配置(按此顺序查找,先找到的生效):
* 1. 环境变量 OPENLIST_URL / OPENLIST_USER / OPENLIST_PASS / BACKUP_PASSPHRASE / OPENLIST_DIR
* 2. ~/.openlist-backup.env
* 3. .workbuddy-backup/openlist-backup.env (已在 .gitignore 内)
*
* 加密文件布局: magic(8) | salt(16) | iv(12) | 密文(...) | GCM tag(16)
*/
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import crypto from 'node:crypto';
import http from 'node:http';
import https from 'node:https';
import { execFileSync } from 'node:child_process';
import { pipeline } from 'node:stream/promises';
import { fileURLToPath } from 'node:url';
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const MAGIC = Buffer.from('BLOGBKP1', 'ascii');
const SCRYPT = { N: 1 << 15, r: 8, p: 1, maxmem: 128 * 1024 * 1024 };
const HEAD = MAGIC.length + 16 + 12;
function arg(name, def) {
const i = process.argv.indexOf('--' + name);
if (i < 0) return def;
const next = process.argv[i + 1];
return next && !next.startsWith('--') ? next : true;
}
const has = (n) => process.argv.includes('--' + n);
function log(...a) {
console.log('[' + new Date().toTimeString().slice(0, 8) + ']', ...a);
}
const mb = (n) => (n / 1048576).toFixed(1) + ' MB';
function loadEnvFile(p) {
if (!fs.existsSync(p)) return {};
const out = {};
for (const line of fs.readFileSync(p, 'utf8').split(/\r?\n/)) {
if (line.trim().startsWith('#')) continue;
const m = /^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$/.exec(line);
if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, '');
}
return out;
}
const cfg = {
...loadEnvFile(path.join(REPO, '.workbuddy-backup', 'openlist-backup.env')),
...loadEnvFile(path.join(os.homedir(), '.openlist-backup.env')),
...Object.fromEntries(
['OPENLIST_URL', 'OPENLIST_USER', 'OPENLIST_PASS', 'BACKUP_PASSPHRASE', 'OPENLIST_DIR']
.filter((k) => process.env[k])
.map((k) => [k, process.env[k]])
),
};
/* ---------- 加密 ---------- */
async function encryptFile(src, dst, passphrase) {
const salt = crypto.randomBytes(16);
const iv = crypto.randomBytes(12);
const key = crypto.scryptSync(passphrase, salt, 32, SCRYPT);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
fs.writeFileSync(dst, Buffer.concat([MAGIC, salt, iv]));
await pipeline(fs.createReadStream(src), cipher, fs.createWriteStream(dst, { flags: 'a' }));
fs.appendFileSync(dst, cipher.getAuthTag());
}
async function decryptFile(src, passphrase, out) {
const size = fs.statSync(src).size;
if (size < HEAD + 16) throw new Error('文件太小,不是有效备份');
const fd = fs.openSync(src, 'r');
const head = Buffer.alloc(HEAD);
fs.readSync(fd, head, 0, HEAD, 0);
const tag = Buffer.alloc(16);
fs.readSync(fd, tag, 0, 16, size - 16);
fs.closeSync(fd);
if (!head.subarray(0, 8).equals(MAGIC)) throw new Error('magic 不匹配,不是本脚本产生的备份');
const salt = head.subarray(8, 24);
const iv = head.subarray(24, 36);
const key = crypto.scryptSync(passphrase, salt, 32, SCRYPT);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
await pipeline(
fs.createReadStream(src, { start: HEAD, end: size - 17 }),
decipher,
fs.createWriteStream(out)
);
}
/* ---------- WebDAV ---------- */
const BASE = new URL(String(cfg.OPENLIST_URL || 'http://127.0.0.1').replace(/\/+$/, ''));
const AUTH = 'Basic ' + Buffer.from((cfg.OPENLIST_USER || '') + ':' + (cfg.OPENLIST_PASS || '')).toString('base64');
const isHttps = BASE.protocol === 'https:';
const transport = isHttps ? https : http;
const DAVDIR = cfg.OPENLIST_DIR || '/本地/git-backup';
// ★ OpenList 的 WebDAV 端点挂在 /dav 下。少了这个前缀会打到普通 HTTP 路由上,
// 表现是 MKCOL/PUT 全部返回 405 Method Not Allowed(很容易误以为是权限问题)。
const DAV_PREFIX = cfg.OPENLIST_DAV_PREFIX || '/dav';
function davReq(method, urlPath, { file, extraHeaders = {} } = {}) {
return new Promise((resolve, reject) => {
const u = new URL(BASE.href.replace(/\/+$/, '') + urlPath.replace(/ /g, '%20'));
const headers = { Authorization: AUTH, ...extraHeaders };
if (file) headers['Content-Length'] = fs.statSync(file).size;
const req = transport.request(
{ hostname: u.hostname, port: u.port || (isHttps ? 443 : 80), path: u.pathname + u.search, method, headers },
(res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks).toString('utf8') }));
}
);
req.on('error', reject);
if (file) {
const rs = fs.createReadStream(file);
rs.on('error', reject);
rs.pipe(req);
} else req.end();
});
}
// prefixEncoded: 已带好并已存在的端点前缀(如 /dav),不参与创建
// relEncoded: 前缀之下、各段已编码的相对目录
async function ensureDir(prefixEncoded, relEncoded) {
let cur = prefixEncoded;
for (const seg of relEncoded.split('/').filter(Boolean)) {
cur += '/' + seg;
const r = await davReq('MKCOL', cur);
if (r.status === 201) { log(' 建目录', decodeURIComponent(cur)); continue; }
// 405 在 WebDAV 里表示「已存在」,但不能无脑相信 —— 真回到普通 HTTP 路由也会给 405。
// 所以补一次 PROPFIND 确认,避免把路径写错(例如漏了 /dav 前缀)当成「目录已存在」而静默放过。
if ([405, 301, 200].includes(r.status)) {
const chk = await davReq('PROPFIND', cur, { extraHeaders: { Depth: '0' } });
if (chk.status >= 400) {
throw new Error(`目录 ${decodeURIComponent(cur)} 既没建成也不存在(MKCOL ${r.status} / PROPFIND ${chk.status})—— 检查 OPENLIST_DIR 与 /dav 前缀`);
}
continue;
}
throw new Error(`建目录失败 ${decodeURIComponent(cur)} → HTTP ${r.status}`);
}
}
function linkNames(xml) {
const out = [];
const re = /<(?:D:|d:)?href>([^<]+)<\/(?:D:|d:)?href>/g;
let m;
while ((m = re.exec(xml))) {
const name = decodeURIComponent(m[1]).replace(/\/+$/, '').split('/').pop();
if (name) out.push(name);
}
return out;
}
function sha256File(p) {
return new Promise((resolve, reject) => {
const h = crypto.createHash('sha256');
const rs = fs.createReadStream(p);
rs.on('data', (c) => h.update(c));
rs.on('end', () => resolve(h.digest('hex')));
rs.on('error', reject);
});
}
const relDirEncoded = DAVDIR.split('/').filter(Boolean).map(encodeURIComponent).join('/');
const davDirPath = DAV_PREFIX + '/' + relDirEncoded;
const RE_BACKUP = /^blog-\d{8}-\d{6}\.bundle(\.enc)?$/;
/* ---------- 子命令:解密 / 列出 ---------- */
const decArg = arg('decrypt', null);
if (decArg) {
const src = path.resolve(String(decArg));
const out = String(arg('out', src.replace(/\.enc$/, '')));
if (!cfg.BACKUP_PASSPHRASE) { console.error('缺少 BACKUP_PASSPHRASE'); process.exit(2); }
await decryptFile(src, cfg.BACKUP_PASSPHRASE, out);
log('解密完成 →', out, mb(fs.statSync(out).size));
log('恢复仓库: git clone "' + out + '" blog-restored');
process.exit(0);
}
if (has('list')) {
await ensureDir(DAV_PREFIX, relDirEncoded);
const ls = await davReq('PROPFIND', davDirPath, { extraHeaders: { Depth: '1' } });
const files = linkNames(ls.body).filter((n) => RE_BACKUP.test(n)).sort().reverse();
log('远端目录', DAVDIR);
for (const f of files) {
const st = await davReq('HEAD', davDirPath + '/' + encodeURIComponent(f));
const when = new Date(st.headers['last-modified'] || Date.now()).toISOString().replace('T', ' ').slice(0, 19);
log(' ' + f + ' ' + mb(Number(st.headers['content-length'] || 0)) + ' ' + when);
}
if (!files.length) log(' (无备份)');
process.exit(0);
}
/* ---------- 主流程 ---------- */
if (!cfg.OPENLIST_URL || !cfg.OPENLIST_USER || !cfg.OPENLIST_PASS) {
console.error('缺少 OpenList 配置(OPENLIST_URL / OPENLIST_USER / OPENLIST_PASS)');
process.exit(2);
}
const ENCRYPT = !has('no-encrypt');
if (ENCRYPT && !cfg.BACKUP_PASSPHRASE) {
console.error('缺少 BACKUP_PASSPHRASE —— 加密备份必须有口令(确实要明文存放请显式加 --no-encrypt)');
process.exit(2);
}
const KEEP = Number(arg('keep', 7)) || 7;
const VERIFY = has('verify');
const DRY = has('dry');
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '-');
const tmpRoot = path.join(REPO, '.workbuddy-backup', 'tmp');
fs.mkdirSync(tmpRoot, { recursive: true });
const tmpDir = fs.mkdtempSync(path.join(tmpRoot, 'run-'));
const rawBundle = path.join(tmpDir, `blog-${stamp}.bundle`);
try {
log('仓库:', REPO);
// 0) 先同步远端引用 —— 见文件头「打包前先 git fetch」那段。
// 不 fetch 就会把过期快照当备份,而且失败是静默的(备份照样报"成功")。
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
if (has('no-fetch')) {
log('0/7 跳过 fetch(--no-fetch)—— 本次打包的是**本地当前 ref** 的快照');
} else {
log('0/7 同步远端引用(git fetch --all --tags --prune)…');
try {
const out = execFileSync('git', ['-C', REPO, 'fetch', '--all', '--tags', '--prune'], {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: gitEnv, timeout: 180000,
});
const tail = (out || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
log(' 完成' + (tail ? ':' + tail : '(无新内容)'));
} catch (e) {
// 离线不该让备份做不出来 —— 降级为用本地已有 ref 打包,但必须显式告警
const why = String(e.stderr || e.stdout || e.message || '').trim().split('\n').filter(Boolean).slice(-2).join(' ');
log(' ★ fetch 失败,降级为「用本地已有 ref 打包」—— 快照可能不含最新提交');
log(' 原因:', why || '(无输出)');
}
}
log('1/7 打包 bundle(git bundle create --all)…');
let t = Date.now();
execFileSync('git', ['-C', REPO, 'bundle', 'create', rawBundle, '--all'], { stdio: ['ignore', 'inherit', 'inherit'] });
log(` 完成 ${mb(fs.statSync(rawBundle).size)} 用时 ${((Date.now() - t) / 1000).toFixed(1)}s`);
// 把「这份快照停在哪」写进日志 —— 恢复时第一件要确认的就是这一行
try {
const head = execFileSync('git', ['-C', REPO, 'log', '-1', '--format=%h %ci %s', 'origin/main'],
{ encoding: 'utf8', env: gitEnv }).trim();
log(' 快照 origin/main =', head);
} catch { /* 没有 origin/main 就不打这行,不影响备份 */ }
let upload = rawBundle;
let finalName = path.basename(rawBundle);
if (ENCRYPT) {
log('2/7 AES-256-GCM 加密(scrypt 派生密钥)…');
const enc = rawBundle + '.enc';
t = Date.now();
await encryptFile(rawBundle, enc, cfg.BACKUP_PASSPHRASE);
fs.unlinkSync(rawBundle);
upload = enc;
finalName = path.basename(enc);
log(` 完成 ${mb(fs.statSync(upload).size)} 用时 ${((Date.now() - t) / 1000).toFixed(1)}s`);
} else {
log('2/7 跳过加密(--no-encrypt)—— 请确认历史里已无敏感文件');
}
const size = fs.statSync(upload).size;
const sha = await sha256File(upload);
log(` 本地 sha256 = ${sha}`);
if (DRY) {
log('--dry:不上传。文件留在', upload);
process.exit(0);
}
log('3/7 准备远端目录', DAVDIR);
await ensureDir(DAV_PREFIX, relDirEncoded);
log('4/7 上传中…');
t = Date.now();
const put = await davReq('PUT', davDirPath + '/' + encodeURIComponent(finalName), {
file: upload, extraHeaders: { 'Content-Type': 'application/octet-stream' },
});
const secs = (Date.now() - t) / 1000;
if (![200, 201, 204].includes(put.status)) throw new Error('上传失败 HTTP ' + put.status);
log(` 完成 ${mb(size)} / ${secs.toFixed(1)}s → ${(size / 1048576 / secs).toFixed(1)} MB/s`);
log('5/7 校验远端');
const stat = await davReq('HEAD', davDirPath + '/' + encodeURIComponent(finalName));
const remoteLen = Number(stat.headers['content-length'] || 0);
if (remoteLen !== size) log(` ★ 远端字节数不一致(本地 ${size} / 远端 ${remoteLen})`);
else log(` 远端字节数一致 (${mb(remoteLen)})`);
if (VERIFY) {
log(' 下载回来比对 sha256…');
const back = path.join(tmpDir, 'readback.bin');
await new Promise((res, rej) => {
const req = transport.request(
{ hostname: BASE.hostname, port: BASE.port || (isHttps ? 443 : 80),
path: davDirPath + '/' + encodeURIComponent(finalName), method: 'GET', headers: { Authorization: AUTH } },
(r) => {
if (r.statusCode !== 200) return rej(new Error('GET ' + r.statusCode));
const ws = fs.createWriteStream(back);
r.pipe(ws); ws.on('finish', res); ws.on('error', rej);
}
);
req.on('error', rej); req.end();
});
const sha2 = await sha256File(back);
log(' ' + (sha2 === sha ? '✓ 读回 sha256 一致 —— 数据完整' : '★ 读回 sha256 不一致 —— 备份已损坏'));
fs.unlinkSync(back);
}
log(`6/7 清理旧份(保留最近 ${KEEP} 份)`);
const ls = await davReq('PROPFIND', davDirPath, { extraHeaders: { Depth: '1' } });
const files = linkNames(ls.body).filter((n) => RE_BACKUP.test(n)).sort().reverse();
log(' 远端现有:', files.join(', ') || '(无)');
for (const old of files.slice(KEEP)) {
const r = await davReq('DELETE', davDirPath + '/' + encodeURIComponent(old));
log(` 删除 ${old} → HTTP ${r.status}`);
}
log('完成。备份文件:', DAVDIR + '/' + finalName);
fs.rmSync(tmpDir, { recursive: true, force: true });
} catch (e) {
console.error('\n失败:', e.message);
console.error('(临时文件保留在 ' + tmpDir + ' 便于排查)');
process.exit(1);
}