☰
Core File Kit fileIo:离线核验日志临时写入与失败收口【鸿蒙心迹】
2026/10/14 19:42:00 网站建设 项目流程

本文的文件名、路径片段、状态、日志和界面均为演示构造,不代表已经在 HarmonyOS 设备上写盘、断电或恢复成功。示例只讨论应用沙箱内的文件处理,不涉及用户公共目录授权或云同步。

一、一个看似正常的“导出成功”提示

我见过的离线审核工具里,最让人难查的问题通常不是根本写不出文件,而是写出了一半,页面却已经提示完成。用户第二天重新打开,看到了一个存在的文件;文件尾巴少了两条关键审计记录。此时若再让应用重新导出,旧文件可能被覆盖,真正的失败现场也就没了。与其把导出按钮做得很漂亮,不如先问一句:哪一个时刻,才值得向用户展示成功?

本轮的第二个 Demo 叫AuditHarbor,界面页面AuditExportPage,任务AL-2213。它只导出三条预置的离线审核事件,版本号固定v7:R-01/START、R-02/CHECK、R-03/CLOSE。目标文件名为audit-AL-2213-v7.jsonl,写入中间文件名为audit-AL-2213-v7.jsonl.part。界面最终状态使用PROMOTED,因为“写完临时文件”和“准备给用户读取的正式文件”应该是两个事件。

这里故意不用文件选择器,也不碰相册。示意路径来自应用上下文的filesDir,不是随手拼一个公共存储路径。Core File Kit 的fileIo负责访问应用文件,但它并不替业务检查审计事件是不是重复、文本中是不是混入了换行、版本是不是已经生成。把格式、安全输入与写盘动作分开,是这篇工程复盘的第一条约束。

二、先决定要写什么,再决定怎么写

示例选用 JSON Lines:每行是一条完整 JSON,便于之后按行解析,也能让单条字段内的换行被 JSON 字符串转义,避免人工拼接冒出伪造的第二条记录。为便于复核,我把三条事件的seq固定为一、二、三;任务 ID 不从输入框自由拼进文件路径,导出版本也由页面的固定流程提供,而不是来自未经校验的任意文件名。

第一段代码解决生成稳定负载的问题。它先校验任务、记录数量、顺序和字段白名单;再按顺序形成 JSONL。实际项目还需要额外校验签名来源、时钟可信度、隐私字段以及允许导出的最大体积。本文不把“JSON 能解析”误说成“审计内容可信”,这两件事是不同层面的校验。

interface AuditRow { seq: number; recordId: string; action: string; } function buildAuditJsonl(taskId: string, rows: AuditRow[]): string { const actions: string[] = ['START', 'CHECK', 'CLOSE']; if (taskId !== 'AL-2213' || rows.length !== 3) { throw new Error('INVALID_BATCH'); } let lines: string[] = []; rows.forEach((row: AuditRow, i: number) => { if (row.seq !== i + 1 || row.recordId !== `R-0${i + 1}` || row.action !== actions[i]) { throw new Error('INVALID_ROW'); } lines.push(JSON.stringify({ taskId: taskId, seq: row.seq, recordId: row.recordId, action: row.action })); }); const payload: string = lines.join('\n') + '\n'; if (payload.length > 4096) { throw new Error('TOO_LARGE'); } return payload; }

这个函数不持有文件句柄,也没有线程需要释放;任何异常都在接触磁盘前返回。长度限制只是演示上限,以字符数量粗筛,不能代替严格的 UTF-8 字节预算。中文、多字节字符或后续增加的附件必须按实际字节数校验。固定的R-01到R-03让案例容易对照,不意味着真实业务只能写三条。批量数据很大时,应切换为流式分段写入并处理背压,而不是用一个巨大字符串占满 UI 线程内存。

三、正式文件只能在读回验证后出现

主要工程动作是先写.part,关闭描述符,再从同一路径读回校验内容,最后才执行重命名。为什么把“关闭”排在读回前?因为我们需要明确写操作已经结束,不应在描述符还活着时混着读同一个文件。为何不用直接覆盖正式文件?因为一旦写盘或校验失败,用户应当继续看到此前有效版本,而不是看到一个中途断掉的半成品。

下面的代码采用单个导出者、固定批次版本和应用沙箱目录三项前提。fileIo.openSync、writeSync、closeSync、readTextSync、renameSync与unlinkSync都属于文件基础操作。它们并不帮我们做版本独占或任务排队;正式文件已存在时直接拒绝复写。代码特意在finally关闭句柄,避免异常分支遗漏释放。这里使用同步 API 是为了凸显操作顺序,生产项目不应在 UI 主线程执行大文件同步 I/O。

import { fileIo } from '@kit.CoreFileKit'; function stageAndPromote(filesDir: string, payload: string): string { const temp: string = `${filesDir}/audit-AL-2213-v7.jsonl.part`; const target: string = `${filesDir}/audit-AL-2213-v7.jsonl`; let file: fileIo.File | null = null; if (fileIo.accessSync(target)) { throw new Error('VERSION_EXISTS'); } try { if (fileIo.accessSync(temp)) { fileIo.unlinkSync(temp); } file = fileIo.openSync(temp, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY); fileIo.writeSync(file.fd, payload); } finally { if (file !== null) { fileIo.closeSync(file); } } try { if (fileIo.readTextSync(temp) !== payload) { throw new Error('READBACK_MISMATCH'); } fileIo.renameSync(temp, target); return target; } catch (error) { if (fileIo.accessSync(temp)) { fileIo.unlinkSync(temp); } throw error; } }

读回检查可抓住短写、尾部残留、编码变更或内容不一致,却不能证明存储介质在突然断电后一定保留最近写入。重命名与彻底持久化不是同一个保证;是否支持同目录替换、错误码如何表达、是否需要额外持久化步骤,应该按设备文件系统及目标 SDK 文档核对。尤其不能把当前示例称为跨文件事务或数据库级原子提交。生产级审计数据若要求断电一致性,应考虑合适的事务存储与恢复协议。

示意 DevEco 白色主题画面中的项目树叫AuditHarbor,核心方法是stageAndPromote,右侧模拟器展示PROMOTED和三条记录;底部 HiLog 也明确标为“演示”。三条日志按业务定义依次是STAGING、READBACK_OK lines=3、PROMOTED v7。这里没有声称它们来自真实的文件系统,也没有用一张成功画面代替故障注入测试。

四、失败不是“弹一个 Toast”就算收口

写盘过程中可能遇到只读目录、空间耗尽、意外退出、同名文件冲突或描述符关闭失败。最重要的不是把每个错误都归为“保存失败”,而是知道已经产生了哪些资源。若打开文件之前校验失败,就没有句柄可关;若写入发生异常,finally必须执行关闭;若读回不一致,就不允许生成正式文件;若关闭句柄本身失败,则也不能直接向页面回报成功。

第三段代码是启动页面时的受限清理示意,只处理本任务、指定版本的临时文件。它不做全目录模糊匹配,更不会删除用户已经导出的正式日志。若此时另一条导出流程正在使用同一个.part,删除就会造成竞态,因此调用者必须先获得单任务串行执行权;本文把“只允许一个导出会话”当成业务前提,而不是系统替我们提供的锁。

function removeOwnStalePart(filesDir: string, busy: boolean): boolean { if (busy) { return false; } const temp: string = `${filesDir}/audit-AL-2213-v7.jsonl.part`; const target: string = `${filesDir}/audit-AL-2213-v7.jsonl`; if (fileIo.accessSync(target)) { return false; } if (!fileIo.accessSync(temp)) { return false; } fileIo.unlinkSync(temp); return true; }

清理函数返回true只说明本轮成功清除了一个指定残留文件,不代表“恢复了审计记录”。如果正式文件已经存在,就不应根据临时文件推测正式文件损坏并覆盖它;如果残留文件包含唯一的一份重要业务数据,也不应直接删除,应该先展示风险并走经过设计的恢复流程。本例只用可重新生成的演示事件,因此可以把孤儿.part当作未提交的工作区。

五、拿三条记录设计一套失败矩阵

正常路径的约定输入是AL-2213/v7,三条事件R-01/START、R-02/CHECK、R-03/CLOSE,目标audit-AL-2213-v7.jsonl。负载构建成功后,状态从READY到STAGING,文件写入且读回一致后变为READBACK_OK,完成重命名才展示PROMOTED。手机图中的“3 条已核验”仅是示例期望结果,不是实际落盘数量的统计截图。

故障验收至少要覆盖五项:构造数据字段非法时不能创建.part;写入一半时没有正式文件;读回少一行时不能重命名;目标文件已存在时不能复写;下一次进入页面时只清理当前任务的废弃.part。每次都应核对句柄是否正确关闭、状态是否回到可重试、原有可用文件是否保持不变。对于真实审核环境,还应把日志校验方式和权限策略作为独立安全需求,不要让演示样例误导为完整审计合规方案。

在工程里,最容易造成事故的是两句不加限定的话:“文件存在就是成功”“重命名了就一定不会丢”。我更愿意保留一个看起来啰嗦的阶段名PROMOTED,让调用者知道它代表应用本次可观察到的写盘流程已完成,但不等于设备级断电可靠性已经经过验证。这样下一次做异步写入、任务队列或用户文件分享时,原有边界也不会被一句“沿用现成方法”带过去。

参考资料:华为开发者联盟 Core File Kit 文件基础操作 API:https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-file-fs;官方应用文件访问说明:https://developer.huawei.com/consumer/en/doc/atomic-guides-V5/atomic-app-file-access-V5。部分在线细项未能完整读取,接口签名应以目标 SDK 为准,尚未进行目标 SDK 编译与实机故障注入。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询