鸿蒙 PC Markdown 编辑器文件系统:Core File Kit 与安全保存

发布时间:2026/7/21 2:24:35

鸿蒙 PC Markdown 编辑器文件系统:Core File Kit 与安全保存 鸿蒙 PC Markdown 编辑器文件系统Core File Kit 与安全保存本文聚焦授权 URI、流式 UTF-8 解码、短写检测、持久化完成点、外部冲突和保存失败恢复。完整示例代码https://gitcode.com/VON-/codex_md_oh。不把文件系统暴露给 Web鸿蒙 PC 上的文档不应由 ArkWeb 直接扫描文件路径。OhMarkdown 由 ArkTS 通过DocumentViewPicker获得用户明确选择的 URI再使用 Core File Kit 读写。Web 编辑器只看到已读取的文本不拥有目录遍历能力。选择器还限制一次只选一个文档并优先展示 Markdown 和文本后缀exportasyncfunctionpickMarkdownDocument(context:Context):PromiseOpenedDocument|undefined{constoptionsnewpicker.DocumentSelectOptions();options.maxSelectNumber1;options.fileSuffixFilters[Markdown|.md,.markdown,.mdown,.mkd,.txt];constdocumentPickernewpicker.DocumentViewPicker(context);constselectedUrisawaitdocumentPicker.select(options);if(selectedUris.length0){returnundefined;}returnreadUtf8Document(selectedUris[0]);}代码来源entry/src/main/ets/shared/services/DocumentService.ets分块读取与 UTF-8 严格校验当前技术验证将单文档上限设为 20MiB每次读取 64KiB。TextDecoder使用fatal: true非法 UTF-8 不会被静默替换成错误字符。同时检查实际读取字节数与stat.size用于识别读取期间文档被外部改变的情况。constdecoderutil.TextDecoder.create(utf-8,{fatal:true,ignoreBOM:false});constchunks:Arraystring[];lettotalBytesRead:number0;while(totalBytesReadstat.size){constrequestedBytesMath.min(READ_CHUNK_BYTES,stat.size-totalBytesRead);constchunknewArrayBuffer(requestedBytes);constbytesReadawaitfileIo.read(file.fd,chunk,{length:requestedBytes});if(bytesRead0){break;}totalBytesReadbytesRead;chunks.push(decoder.decodeToString(newUint8Array(chunk,0,bytesRead),{stream:totalBytesReadstat.size}));}代码来源entry/src/main/ets/shared/services/DocumentService.ets为什么写入后还要 truncate 和 fsyncexportasyncfunctionwriteUtf8Document(uri:string,content:string,format:DocumentFormatcreateDefaultDocumentFormat()):Promisestring{constfileawaitfileIo.open(uri,fileIo.OpenMode.READ_WRITE);try{constserializedContentserializeDocument(content,format);constexpectedBytesbuffer.from(serializedContent,utf-8).length;constwrittenBytesawaitfileIo.write(file.fd,serializedContent,{offset:0,encoding:utf-8});if(writtenBytes!expectedBytes){thrownewError(The complete document could not be written.);}awaitfileIo.truncate(file.fd,writtenBytes);awaitfileIo.fsync(file.fd);returnfile.name;}finally{awaitfileIo.close(file);}}代码来源entry/src/main/ets/shared/services/DocumentService.ets如果新内容比旧文件短只从 offset 0 覆盖会在文件尾部留下旧字节因此必须truncate。fsync则要求系统将写入同步到存储设备不只留在进程缓冲中。写入长度也按 UTF-8 字节而不是 JavaScript 字符数校验否则中文文档会得到错误结论。鸿蒙 PC 应用内的保存后状态下图来自鸿蒙 PC / 2in1 模拟器中的应用内部界面。文档正常保存并重启后编辑区回到干净会话没有出现未完成保存或崩溃恢复提示状态栏保留 UTF-8 与 LF 格式信息。系统选择器负责授予 URI应用内部则负责呈现当前文档和持久化状态。“可保存”不等于“已达最终安全保存”基础实现完成用户 URI 的原位写入闭环并增加保存前沙箱备份、外部修改冲突检测和失败后恢复。但受授权 URI 能力限制目标文件仍不能保证像应用沙箱中的AtomicFile那样完成同目录临时文件原子替换。磁盘写满、中途异常、只读 URI 和权限失效仍需用故障注入持续验证。URI 是授权句柄不是普通路径字符串系统选择器返回的 URI 表达用户对某个文档的明确授权。应用应保存和使用这个 URI而不是从显示名称拼接一个猜测路径更不能因为需要文件列表就申请无限制的全盘访问。URI 可能由不同文档提供方实现其权限、生命周期和路径表现并不等同于 POSIX 文件。这会影响错误处理同一个 URI 可能在下次启动时权限失效文档可能被外部移动提供方也可能拒绝某种打开模式。应用不能把所有异常都显示为“文件不存在”需要区分用户取消、无权限、格式不支持、文件过大、读取中变化和写入失败。文件名只能用于界面和默认导出名不能作为文件身份。两个目录可以存在同名README.md重命名也不应让编辑器把它误认为另一份缓存。当前会话的核心标识应是授权 URI工作区条目则保留父目录关系和真实子 URI。打开流程为什么先检查再解码读取一个 Markdown 文档看似只需要read实际至少包含以下步骤使用授权 URI 打开只读文件句柄。获取stat.size在分配大缓冲区前执行 20MiB 上限检查。以 64KiB 分块读取避免一次分配与文件等大的 ArrayBuffer。使用流式 UTF-8 解码处理多字节字符跨块边界的情况。比较累计读取字节与初始大小识别读取过程中截断或变化。检测 BOM 和行尾格式正文与格式元数据分别保存。无论成功失败都关闭文件句柄。流式解码中的stream参数很关键。一个中文字符可能有三个 UTF-8 字节刚好被两个 64KiB 块分开如果每块独立解码边界字符可能变成替换符。TextDecoder保留未完成字节直到下一块到达最后一块再结束解码状态。fatal: true表示遇到非法 UTF-8 就失败而不是用悄悄替换。对编辑器而言静默替换后再保存会永久改变用户文件因此明确拒绝并提示“当前仅支持有效 UTF-8”更安全。未来支持其他编码时也应通过可识别的编码策略打开不能降低为任意字节猜测。文件大小上限要在多个层次一致20MiB 是当前单文档保护上限5MiB 是 Web 编辑器的大文档降级阈值两者含义不同。前者阻止应用读取超出当前验证范围的文件后者允许继续打开但关闭高成本能力。大小还存在字节和字符差异。stat.size是文件字节数JavaScriptcontent.length接近 UTF-16 代码单元数量中文和 emoji 下二者不相等。读取上限应按字节判断Bridge 快照上限和编辑扩展阈值则按内存模型选择并明确单位保存完整性必须比较 UTF-8 编码后的字节数。如果各层都写一个含义不明的MAX_SIZE很容易出现原生允许 20MiB、Bridge 只接受 5MiB、界面却仍承诺完整恢复。常量名称、错误文案和测试语料应把单位与目的写清楚。原位写入为什么有截断风险目标 URI 只提供读写句柄时常见保存方式是从 offset 0 写入。如果新正文短于旧正文而不调用truncate尾部会残留旧字节。例如原文件是# title\nlong paragraph新文件只有# title覆盖前七个字节并不会自动删除剩余内容。完整写入也不能只看 API 没有抛错。底层可能出现短写所以实现计算 UTF-8 期望字节数并比较返回值。中文字符串的content.length不能代替字节数否则多个汉字会导致错误判断。写完后执行fsync再把保存完成传回编辑器只有到这个点界面才有资格把 Modified 改为 Saved。即便完成write truncate fsync进程如果在原位覆盖中途终止目标文件仍可能已经部分改变。这就是为什么安全保存还需要保存前的旧内容副本。保存前沙箱备份与启动恢复当前实现会在写用户 URI 前重新读取磁盘版本并把旧正文、BOM、行尾、文档 URI 和时间写入应用沙箱的待处理保存备份。备份使用AtomicFile提交避免备份 JSON 自身只写了一半。随后才执行目标 URI 写入。保存成功后清除备份保存失败时立即尝试把旧版本写回目标 URI。如果即时恢复也失败备份不会删除。应用下次启动会发现这份记录提示用户保留当前文件或恢复先前版本。这样至少把“目标文件可能受损”的事实从一次临时错误变成可继续处理的状态。这套方案不是目标文件的原子替换。真正理想的保存是在同一文件系统创建临时文件、完整写入并同步再用原子 rename 替换目标。但系统授权 URI 不一定允许创建同目录临时文件或重命名。工程上不能声称不存在的原子能力因此使用沙箱旧版本作为降级保护并把保存途中强杀作为设备故障用例。保存前重新读取用于发现外部冲突桌面用户可能同时用终端、版本控制或另一款编辑器修改同一文件。如果 OhMarkdown 打开文件后一直只保留旧基线几分钟后直接保存会覆盖外部变化。当前保存流程会重新读取磁盘比较内容与打开时记录的持久化基线同时比较 BOM 和行尾元数据。如果磁盘版本已经变化应用阻止覆盖并提示用户重新打开。此时宁可让用户手动合并也不能把外部编辑静默抹掉。未来可以提供三方合并打开基线、本地未保存版本、当前磁盘版本。但“强制覆盖”必须是明确的二次动作并最好先保留磁盘备份。仅比较修改时间不够可靠因为时间精度、同步工具和内容相同重写都会产生边界当前文本与格式比较虽然有成本却更符合 20MiB 范围内的数据安全优先级。BOM 与换行属于保存契约文档打开后编辑器正文与DocumentFormat分开保存。UTF-8 BOM 不进入 CodeMirror 光标空间CRLF 则通过行分隔符配置和保存序列化保留。普通 LF、CRLF 文件编辑后应继续使用原格式没有换行的单行文件保持NONE语义。Mixed EOL 更复杂因为编辑后新增行无法自动知道应使用哪一种历史风格。当前保存时明确询问用户将文档规范化为 LF 或 CRLF而不是悄悄选择。这个决定会改变较多字节因此必须出现在保存路径并更新后续持久化基线。格式状态显示在状态栏不是装饰它让用户在保存前知道应用识别到的编码和行尾。后续字节级夹具会用 SHA-256 和十六进制验证 BOM、中文、尾随换行与 CRLF 往返而不能只用编辑器显示内容相同作为结论。未保存切换与系统取消也属于文件正确性点击 Open、New、工作区中的另一个文件或关闭窗口时如果当前文档已修改应用必须先确认用户意图。系统选择器取消不应清空当前文档也不应把状态改成错误打开失败时原文档和撤销历史应保持不变。保存对话框同样有取消路径。新文档在用户取消目标选择后仍然是未保存文档恢复快照继续存在。只有实际写入成功才记录新的 URI、文件名和持久化基线。把“用户取消”和“系统失败”分开可以避免令人不安的错误提示也让自动化用例更准确。建议的文件故障测试矩阵场景预期结果选择器取消当前文档、脏状态和撤销栈不变非法 UTF-8明确拒绝打开不生成替换字符超过 20MiB在读取全文前拒绝并说明上限新内容短于旧内容保存后没有旧尾部字节中文与 emoji实际写入字节数校验正确保存前文件被外部修改阻止覆盖并保留本地未保存内容写入发生短写不标记 Saved尝试恢复旧版本写入过程中强制停止重启后检测待处理备份并提供恢复只读或权限过期 URI显示可处理错误草稿仍可恢复BOM/CRLF 文档编辑保存后格式与正文符合策略Mixed EOL保存前明确选择 LF 或 CRLF磁盘空间不足不清除恢复记录不声称保存成功这些用例要区分纯函数、模拟器和真机。序列化、格式检测可以自动化系统选择器、授权 URI 和强杀需要鸿蒙设备磁盘写满、断电和特殊文档提供方最好在可控环境做故障注入。文件能力验收清单Web 页面没有直接文件访问所有 URI 都来自用户授权或已授权工作区。打开前检查字节大小分块读取并使用严格流式 UTF-8 解码。文件读取期间变化、非法编码和超限都有明确错误。保存按 UTF-8 字节验证短写随后 truncate 和 fsync。保存成功点与界面 Saved 状态一致不在 Bridge 接收时提前完成。写入前保留原磁盘版本失败后恢复未解决备份在重启时可发现。保存前检测外部修改默认不覆盖未知的新磁盘内容。BOM、LF、CRLF、Mixed EOL 的策略可见且可测试。取消、失败、权限失效不会清空当前编辑内容和恢复草稿。测试报告说明 URI 提供方、HAP 哈希和设备不把普通路径测试等同于系统授权测试。鸿蒙 PC 文件编辑的底线不是“按钮点击后没有报错”而是用户能够知道打开了什么、写入了什么、发生冲突时保住了什么。把授权、编码、字节完整性、持久化完成点和故障恢复连成一条链Markdown 编辑器才真正具备处理长期本地资料的资格。文件操作需要串行化同一文档的写入用户可能连续按保存、快捷键自动重复或保存尚未完成时触发导出和关闭。两个写任务同时使用同一 URI会让 truncate、fsync 和备份清理交错最终状态不可预测。每个文档会话应只有一个持久化任务重复保存可以合并为最新 revision 或排队但不能并发覆盖。串行化还要处理“保存期间继续编辑”。发送给文件服务的内容快照绑定 revision写入完成后如果当前 revision 已增加只更新持久化基线为已写快照界面仍保持 Modified。下一次保存再写最新内容。否则一次慢保存会错误清除后来输入的脏状态。关闭窗口时若仍在持久化应等待可控时间或明确提示不直接销毁状态。强杀无法等待因此恢复快照和保存备份必须覆盖这条异常路径。持久化语义不能夸大 fsyncfsync请求把文件数据同步到存储但不同文档提供方、文件系统和设备仍可能有缓存或远端同步层。API 成功表示应用已完成可用的本地持久化步骤不表示云盘已上传也不保证设备物理损坏后一定恢复。同样保存备份位于应用沙箱应用卸载会清除设备损坏也无法帮助。产品文案应准确表达“保存到所选文档”和“已保留可恢复旧版本”不声称绝对防丢失。重要资料仍需要用户自己的版本控制或备份体系。工程测试可以验证 API 返回、重新打开、进程强停和文件字节无法模拟的硬件/提供方保证应明确留在边界之外。文件夹工作区扩大了授权边界打开单文件时 URI 权限只覆盖该文档打开文件夹后应用可以列出授权目录并构造子项 URI。目录树应只展示文件夹和支持的 Markdown/文本类型按需展开限制一次读取条目数避免递归扫描大目录阻塞界面。子项名称必须拒绝路径分隔符和非法目录跳转URI 应通过结构化 API生成不能字符串拼接。符号链接可能指向授权根之外需要根据 Core File Kit 实际语义验证在策略明确前不递归跟随未知链接。工作区授权不等于建立全盘索引。搜索和大纲应限制在用户选择范围错误或权限变化只影响相关节点不清空当前已打开正文。目录刷新发现当前文件被外部删除时也要保留未保存编辑会话并允许另存。新建与另存为有不同身份变化新建文档最初没有 URI恢复快照使用临时会话身份。第一次保存成功后才绑定选择器返回的 URI、文件名和持久化格式。用户取消选择时仍是未命名脏文档。“保存”覆盖当前 URI“另存为”创建新目标并在成功后决定是否把当前会话切换到新 URI。若新目标写入失败原 URI、基线和标签不能提前改变。导出 HTML 则永远不改变 Markdown 会话身份。覆盖已有目标时系统选择器可能自行确认但应用仍应按目标 URI 做完整备份与写入。默认文件名要过滤路径字符和控制字符扩展名策略清楚不能直接使用不可信标题形成路径。文件编码支持应采用显式扩展当前严格支持 UTF-8遇到非法序列拒绝。未来支持 GB18030、UTF-16 等编码时需要可靠检测或用户选择并把编码加入DocumentFormat。自动猜测不可避免存在误判保存前应让用户看到当前编码。转换到 UTF-8属于主动格式转换不应在普通保存中悄悄发生。原编码无法表示新字符时要提供另存 UTF-8 或取消不能用问号替换。BOM、字节序和换行都需要对应夹具。实现上不要先用宽松 UTF-8 生成替换字符再尝试其他编码那会丢失原始字节证据。应基于原始字节选择解码器失败时保持文件未改。外部修改后的合并模型阻止覆盖是安全的第一步但桌面用户最终需要解决冲突。三方合并需要三份输入打开时持久化基线、本地 CodeMirror 内容、保存前重新读取的磁盘内容。算法输出冲突块由用户确认后形成新的本地正文。合并不能只按行尾规范化后保存而忘记格式。基线、磁盘和本地各自的 BOM/行尾需要策略通常以当前磁盘格式或用户选择为目标。二进制/非法编码、超大文件和基线缺失时降级为另存而不是冒险自动合并。冲突界面属于高风险功能应在单文件保存和备份完全验证后实现。未完成前明确提示“文件已在外部修改请重新打开或另存为”比一个不可靠合并器更好。文件服务的可测试性设计系统 URI 很难稳定制造短写、空间不足和权限中途失效。文件服务可以把选择器、读写句柄和原子沙箱存储分层使纯格式与状态逻辑使用内存替身测试设备层再验证真实 API。故障注入替身应能在 open、read、write、truncate、fsync 和 close 指定失败返回短写或延迟完成。每个注入点断言目标旧内容、沙箱备份、恢复快照、持久化基线和界面状态。设备内部构建再用暂停点命中强杀窗口。替身不能让生产代码走完全不同路径。注入接口只替换底层操作上层保存顺序和状态机保持一致正式构建去除调试入口。用户可见错误需要可行动“I/O error”不能帮助用户保护资料。错误文案应说明发生在打开还是保存、原文件是否可能变化、未保存内容是否仍保留以及下一步可以重试、另存还是恢复旧版本。底层错误码进入诊断界面不展示长堆栈。权限失效提示重新选择或另存外部冲突提示重新打开非法 UTF-8说明当前支持范围文件过大说明上限和大文件策略恢复失败则强调沙箱备份仍保留或已无法读取。文案也不能做无法验证的保证。例如即时恢复写入失败时不能说“原文件已恢复”应说检测到未完成保存并保留旧版本备份。清楚的状态是数据安全的一部分。

相关新闻