前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

doc_text 在解析 .docx 文件时会产生临时文件——因为 zlib.decompressFile 需要把 ZIP 内容解压到磁盘上。这些临时文件用完之后必须清理,否则会占用存储空间。这篇讲 doc_text 的临时文件管理策略,包括创建、使用、清理的完整生命周期,以及一些容易踩的坑。

一、临时目录的创建

1.1 命名策略

const tempDir = filePath + "_temp";
// 例如:"/data/storage/el2/base/test.docx" → "/data/storage/el2/base/test.docx_temp"

1.2 创建代码

try {
  fs.mkdirSync(tempDir);
} catch (e) {
  // 目录可能已存在
}

1.3 为什么用 try-catch 包裹

场景 mkdirSync 行为 catch 处理
目录不存在 创建成功 不触发
目录已存在 抛出异常 静默忽略
父目录不存在 抛出异常 静默忽略
权限不足 抛出异常 静默忽略

1.4 目录已存在的场景

什么时候临时目录会已经存在?
1. 上一次解析同一个文件时崩溃了,没来得及清理
2. 两次解析同一个文件,第一次的清理还没完成
3. 用户手动创建了同名目录(极少见)

💡 静默忽略"目录已存在"的异常是合理的——如果目录已经存在,decompressFile 会覆盖里面的文件,不影响功能。但如果是权限不足导致创建失败,后续的 decompressFile 也会失败,会被外层 catch 捕获。

二、临时目录的使用

2.1 解压到临时目录

await zlib.decompressFile(filePath, tempDir);

解压后的目录结构:

test.docx_temp/
├── [Content_Types].xml
├── _rels/
│   └── .rels
├── word/
│   ├── document.xml      ← 我们要读的
│   ├── styles.xml
│   ├── fontTable.xml
│   ├── settings.xml
│   └── _rels/
│       └── document.xml.rels
└── docProps/
    ├── core.xml
    └── app.xml

2.2 从临时目录读取文件

const documentXmlPath = tempDir + "/word/document.xml";
if (!fs.accessSync(documentXmlPath)) {
  this.cleanupTempDir(tempDir);
  return null;
}

const xmlFile = fs.openSync(documentXmlPath, fs.OpenMode.READ_ONLY);
const xmlStat = fs.statSync(documentXmlPath);
const xmlBuf = new ArrayBuffer(xmlStat.size);
fs.readSync(xmlFile.fd, xmlBuf);
fs.closeSync(xmlFile);

2.3 只读取 document.xml

文件 是否读取 原因
word/document.xml 正文文本在这里
word/styles.xml 样式信息,不需要
word/fontTable.xml 字体信息,不需要
word/header1.xml 页眉,当前不提取
word/footer1.xml 页脚,当前不提取
docProps/core.xml 文档属性,不需要

📌 解压了整个 ZIP 但只读了一个文件。这是 zlib.decompressFile API 的限制——它不支持只解压特定文件。如果能只解压 word/document.xml,可以节省 I/O 和存储空间。

三、cleanupTempDir 递归删除

3.1 完整实现

private cleanupTempDir(dirPath: string): void {
  try {
    const files = fs.listFileSync(dirPath);
    for (const file of files) {
      const fullPath = dirPath + "/" + file;
      const stat = fs.statSync(fullPath);
      if (stat.isDirectory()) {
        this.cleanupTempDir(fullPath);  // 递归删除子目录
      } else {
        fs.unlinkSync(fullPath);        // 删除文件
      }
    }
    fs.rmdirSync(dirPath);              // 删除空目录
  } catch (e) {
    // 忽略清理错误
  }
}

3.2 执行流程

cleanupTempDir("test.docx_temp")
│
├── listFileSync → ["[Content_Types].xml", "_rels", "word", "docProps"]
│
├── "[Content_Types].xml" → isDirectory? No → unlinkSync
│
├── "_rels" → isDirectory? Yes → cleanupTempDir("test.docx_temp/_rels")
│   ├── listFileSync → [".rels"]
│   ├── ".rels" → unlinkSync
│   └── rmdirSync("test.docx_temp/_rels")
│
├── "word" → isDirectory? Yes → cleanupTempDir("test.docx_temp/word")
│   ├── listFileSync → ["document.xml", "styles.xml", ..., "_rels"]
│   ├── "document.xml" → unlinkSync
│   ├── "styles.xml" → unlinkSync
│   ├── "_rels" → cleanupTempDir → 递归删除
│   └── rmdirSync("test.docx_temp/word")
│
├── "docProps" → isDirectory? Yes → cleanupTempDir → 递归删除
│
└── rmdirSync("test.docx_temp")

3.3 使用的 fs API

API 作用 说明
fs.listFileSync(dir) 列出目录内容 返回文件名数组
fs.statSync(path) 获取文件信息 判断是文件还是目录
fs.unlinkSync(path) 删除文件 不能删除目录
fs.rmdirSync(dir) 删除空目录 目录必须为空

3.4 删除顺序

必须先删除目录中的所有文件和子目录,才能删除目录本身。
这就是为什么需要递归——先深入最内层,从里往外删。

错误顺序:
rmdirSync("test.docx_temp")  → 失败!目录不为空

正确顺序:
unlinkSync("test.docx_temp/word/document.xml")
unlinkSync("test.docx_temp/word/styles.xml")
rmdirSync("test.docx_temp/word")
// ... 删除所有子目录
rmdirSync("test.docx_temp")  → 成功!

四、异常安全

4.1 try-catch 包裹

private cleanupTempDir(dirPath: string): void {
  try {
    // 所有清理逻辑
  } catch (e) {
    // 忽略清理错误
  }
}

4.2 为什么忽略清理错误

清理错误 影响 处理
文件被占用 临时文件残留 下次覆盖
权限不足 临时文件残留 无法处理
文件已被删除 无影响 忽略
目录不存在 无影响 忽略

4.3 清理失败不应影响主流程

// 正确:清理失败不影响返回结果
const text = this.parseDocxXml(xmlBuf);
this.cleanupTempDir(tempDir);  // 即使失败,text 已经拿到了
return text;

// 如果清理抛异常且没有 catch:
const text = this.parseDocxXml(xmlBuf);
this.cleanupTempDir(tempDir);  // 抛异常!
return text;  // 永远执行不到!

💡 清理操作永远不应该影响主流程的返回值。即使临时文件没删干净,文本已经成功提取了,应该正常返回。

五、文件句柄管理

5.1 openSync / closeSync 配对

// 模式1:读取原始文件
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
// ... 使用 file.fd ...
fs.closeSync(file);

// 模式2:读取 XML 文件
const xmlFile = fs.openSync(documentXmlPath, fs.OpenMode.READ_ONLY);
// ... 使用 xmlFile.fd ...
fs.closeSync(xmlFile);

5.2 文件句柄泄漏的风险

// 当前代码的潜在问题:
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
const stat = fs.statSync(filePath);
const buf = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buf);  // 如果这里抛异常...
fs.closeSync(file);          // 这行不会执行 → 句柄泄漏!

5.3 改进方案

// 使用 try-finally 保证关闭
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
try {
  const stat = fs.statSync(filePath);
  const buf = new ArrayBuffer(stat.size);
  fs.readSync(file.fd, buf);
} finally {
  fs.closeSync(file);  // 无论成功失败都关闭
}
方案 优点 缺点
当前(无 finally) 简单 异常时句柄泄漏
try-finally 安全 多几行代码

5.4 实际影响

文件句柄泄漏的影响:
1. 短期:无明显影响(系统有句柄上限,通常几千个)
2. 长期:如果频繁调用且频繁失败,可能耗尽句柄
3. 进程退出时:系统自动回收所有句柄

对于 doc_text 的使用场景(偶尔调用),句柄泄漏的风险很低。

六、与 Android 端的对比

6.1 Android 端:无临时文件

// Android 使用 POI,在内存中处理
FileInputStream fis = new FileInputStream(filePath);
XWPFDocument docx = new XWPFDocument(fis);  // 内存中解压 ZIP
// 不产生临时文件
fis.close();

6.2 对比

维度 OpenHarmony Android
ZIP 解压方式 解压到磁盘 在内存中解压
临时文件
需要清理
内存占用 高(整个 ZIP 在内存中)
磁盘 I/O 高(写临时文件)
实现复杂度 中(需要清理逻辑)

6.3 为什么 OpenHarmony 不能在内存中解压

// zlib.decompressFile 的 API 签名
zlib.decompressFile(inFile: string, outFile: string): Promise<void>

// 它只支持文件到文件的解压
// 不支持文件到内存的解压
// 所以必须用临时目录

📌 如果 OpenHarmony 的 zlib 模块提供了内存解压 API(比如 decompressToBuffer),就可以避免临时文件。但当前版本没有这个 API。

七、临时文件残留的处理

7.1 残留场景

场景 原因 临时文件状态
正常完成 cleanupTempDir 成功 ✅ 已清理
解析异常 外层 catch 捕获,但没调用 cleanup ❌ 残留
应用崩溃 进程终止 ❌ 残留
清理失败 权限或文件占用 ❌ 残留

7.2 残留文件的影响

残留的临时目录:test.docx_temp/
大小:通常几 KB 到几 MB(取决于 docx 文件大小)

影响:
1. 占用存储空间(通常很小)
2. 下次解析同一文件时会覆盖(不影响功能)
3. 不会影响其他文件的解析

7.3 改进方案

// 方案1:使用 try-finally 保证清理
const tempDir = filePath + "_temp";
try {
  fs.mkdirSync(tempDir);
  await zlib.decompressFile(filePath, tempDir);
  // ... 读取和解析 ...
  return text;
} finally {
  this.cleanupTempDir(tempDir);
}

// 方案2:启动时清理旧的临时目录
onAttachedToEngine(binding: FlutterPluginBinding): void {
  // 清理可能残留的临时目录
  this.cleanupOldTempDirs();
}

八、最佳实践总结

8.1 临时文件管理的原则

  1. 及时清理:用完立即删除
  2. 异常安全:try-finally 保证清理
  3. 静默失败:清理失败不影响主流程
  4. 可预测命名:临时目录名可以从源文件名推导
  5. 递归删除:处理嵌套目录结构

8.2 doc_text 的改进建议

在这里插入图片描述

总结

doc_text 的临时文件管理涵盖了创建、使用、清理的完整生命周期:

  1. 创建:filePath + “_temp”,mkdirSync + try-catch
  2. 使用:zlib.decompressFile 解压,只读取 document.xml
  3. 清理:cleanupTempDir 递归删除,忽略清理错误
  4. 句柄管理:openSync / closeSync 配对,但缺少 finally 保护
  5. 残留处理:异常场景可能残留,但影响有限

下一篇我们看错误处理体系——三种错误码、各种边界场景的防御策略。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐