feat(data): 命令行导入导出通道 + 修上传解析的句柄泄漏

- scripts/data-transfer.js:tables / export / import --preview|--apply,与网页版共用同一套内核。
  用于 nginx/CDN 挡住 1.22MB 上传时的备用通道,也可挂 cron 做异地备份。
- readBundle:校验失败中途抛错时关闭 readStream 与 readline,避免反复上传坏包耗尽文件句柄。
- docs:补命令行用法与体积限制说明。
This commit is contained in:
Drsakura
2026-10-23 13:48:53 +08:00
parent 1a759ef37d
commit 1e7c3fdcc1
3 changed files with 203 additions and 1 deletions
+17 -1
View File
@@ -217,6 +217,22 @@ function openMaybeGzip(filePath) {
return fs.createReadStream(filePath).pipe(isGzip ? zlib.createGunzip() : new (require('stream').PassThrough)())
}
/**
* 逐行吐出,并在**任何**退出路径上关掉底层流。
*
* 校验失败时 readBundle 会中途抛错,如果不在这里兜住,文件句柄就跟着异常一起漏掉 ——
* 服务器上反复上传坏包会把 fd 耗光(排查起来极难,因为错误信息只有「数据包损坏」)。
* 包在外层 for-await 里的生成器,在 break / throw / 正常结束三种情况下都会走到 finally。
*/
async function* linesOf(reader, input) {
try {
for await (const line of reader) yield line
} finally {
try { reader.close() } catch { /* 已经关了 */ }
try { input.destroy() } catch { /* 已经释放 */ }
}
}
/**
* 读取并校验数据包。
*
@@ -241,7 +257,7 @@ async function readBundle(filePath) {
const counts = new Map()
let lineNo = 0
for await (const raw of reader) {
for await (const raw of linesOf(reader, input)) {
const line = raw.trim()
if (!line) continue
lineNo++