鸿蒙 PC Markdown 编辑器分享缓存生命周期治理
鸿蒙 PC Markdown 编辑器分享缓存生命周期治理仓库地址https://gitcode.com/VON-/codex_md_oh代码基线系统分享实现6c823eb缓存生命周期收口2b82214。系统分享为什么需要本地快照鸿蒙 PC Markdown 编辑器发起系统分享时不能把工作区原文件 URI 直接交给任意接收方。原文件可能位于用户授权目录URI 权限范围比一次分享更大当前标签还可能包含尚未保存的正文磁盘内容并不是用户此刻看到的版本。OhMarkdown 因此从 CodeMirror 当前缓冲区获取快照在应用cacheDir中生成独立 Markdown 文件只给本次系统目标临时读取这一条 URI。这个方案同时保护两个事实分享内容与当前编辑器一致接收方又拿不到工作区原始授权。代价是缓存目录会出现含有真实正文的share-*.md。如果应用只依赖系统将来某个时刻清理 cacheDir不同文档名会不断产生不同文件本地敏感内容的留存时间和数量都没有应用级保证。缓存文件位于沙箱并不等于可以永久保留。攻击面不仅来自其他应用直接访问还包括设备备份、调试工具、崩溃取证、越狱环境和后续代码误用。安全设计应按“完成任务所需的最小数据、最短时间、最小能力”处理而不是把沙箱当作无限期档案库。生命周期问题不是普通临时文件清理分享快照与普通构建缓存不同。系统分享面板打开后接收应用可能稍后才读取 URI如果 OhMarkdown 在startAbility返回后立即删除文件某些目标会看到空文件或读取失败。如果从不删除用户连续分享不同文章又会积累正文。生命周期治理必须在可用性与最小留存之间设置明确边界。本轮采用双重限制快照最多保留最近 32 份文件超过 24 小时即进入清理范围。当前正在分享的文件始终保留不因年龄或数量被本轮清理。这个策略不是声称接收方一定在 24 小时内读取而是给 Beta 建立一个保守、可测试的应用级上限真机兼容目标验证仍决定后续是否调整期限。数量和时间缺一不可。只有 24 小时过期用户在一天内批量分享数百个不同文件时仍会形成短期峰值只有 32 份上限长期不再使用的 32 份敏感正文可能永久留下。两条规则共同限制稳态与突发规模。清理范围必须采用严格命名空间cacheDir可能包含图片缩略图、Web 缓存、系统组件文件或未来其他功能的临时数据。分享清理不能把整个目录当作自己所有。OhMarkdown 只处理名称同时满足share-前缀和.md后缀的条目并再次拒绝斜杠、反斜杠和 NUL。constSHARE_CACHE_PREFIX:stringshare-;constSHARE_CACHE_SUFFIX:string.md;constMAX_SHARE_CACHE_FILES:number32;constSHARE_CACHE_RETENTION_MILLISECONDS:number24*60*60*1000;if(!name.startsWith(SHARE_CACHE_PREFIX)||!name.endsWith(SHARE_CACHE_SUFFIX)||name.includes(/)||name.includes(\\)||name.includes(\u0000)){continue;}文件名白名单不是路径安全的全部。枚举后仍使用lstat目录与符号链接直接跳过。这样即使缓存目录中出现名为share-example.md的目录或者某个异常过程创建了指向其他位置的链接清理器也不会递归进入或跟随目标。为什么要跳过符号链接删除 API 接收的是路径。若应用只看名字而跟随符号链接理论上可能对预期之外的对象执行操作具体风险取决于平台unlink语义和沙箱能力。安全代码不需要押注系统恰好阻止危险行为而是先用lstat明确对象类型。constpath:string${cacheDirectory}/${name};try{conststatawaitfileIo.lstat(path);if(stat.isDirectory()||stat.isSymbolicLink()){continue;}entries.push({name:name,path:path,modifiedTime:normalizeModifiedTimeMilliseconds(stat.mtime)});}catch(_){}这里的异常隔离也很重要。目录正在被系统或另一个操作修改时某个条目可能在listFile与lstat之间消失。单个条目失败不应让用户本次分享失败也不应阻止其他可清理条目继续处理。修改时间存在秒与毫秒陷阱JavaScript 的Date.now()返回毫秒而 HarmonyOS 当前模拟器的stat.mtime返回秒级 Unix 时间。如果直接相减新创建文件会得到一个约一千倍量级的“年龄”立即被判定超过 24 小时。第一次设备测试正好捕获了这个问题连续生成 36 份快照后只剩当前 1 份而预期是 32 份。实现没有把失败归咎于模拟器也没有放宽断言而是统一时间单位。十位以下的正时间按秒转换为毫秒已经是毫秒的值原样保留。functionnormalizeModifiedTimeMilliseconds(modifiedTime:number):number{returnmodifiedTime0modifiedTime10_000_000_000?modifiedTime*1000:modifiedTime;}阈值10_000_000_000大于当前秒级时间戳、远小于当前毫秒级时间戳足以区分两个平台表现。这个修复还说明系统 API 的时间单位必须在设备上验证不能仅凭类型都是number就假设语义相同。排序决定谁被保留清理器按修改时间从新到旧排序时间相同时再按名称稳定排序。遍历中维护已保留数量当前分享路径优先保留未过期且数量未达 32 的条目继续保留其余条目尝试删除。entries.sort((left:ShareCacheEntry,right:ShareCacheEntry):numberright.modifiedTime-left.modifiedTime||right.name.localeCompare(left.name));letretainedCount:number0;for(constentryofentries){if(entry.pathpreservedPath){retainedCount1;continue;}constexpired:booleannow-entry.modifiedTimeSHARE_CACHE_RETENTION_MILLISECONDS;if(!expiredretainedCountMAX_SHARE_CACHE_FILES){retainedCount1;continue;}awaitfileIo.unlink(entry.path).catch((){});}当前路径不参与淘汰是硬约束。即使设备时钟跳变、旧同名文件时间异常或目录已达到上限本次即将发送的 URI 都必须存在。其余文件按最新优先符合用户更可能继续使用最近分享内容的经验假设。清理为什么放在完整写入之后分享快照先创建、写入、检查完整 UTF-8 字节数、截断并fsync关闭句柄后才运行清理。最后把当前路径转换为只读分享 URI。这样清理器可以把当前文件纳入总数量又不会在正文尚未持久化时把路径交给系统。constfileawaitfileIo.open(path,fileIo.OpenMode.CREATE|fileIo.OpenMode.READ_WRITE);try{constexpectedBytes:numberbuffer.from(content,utf-8).length;constwrittenBytes:numberawaitfileIo.write(file.fd,content,{offset:0,encoding:utf-8});if(writtenBytes!expectedBytes){thrownewError(The complete share document could not be written.);}awaitfileIo.truncate(file.fd,writtenBytes);awaitfileIo.fsync(file.fd);}finally{awaitfileIo.close(file);}awaitcleanupMarkdownShareCache(cacheDirectory,path);returnfileUri.getUriFromPath(path);这条顺序保持了文件功能的基本原则只有完整数据才能产生成功 URI。清理是安全增强不应该让未完成写入变成可分享对象也不能改变原始 Markdown 工作区文件。清理失败为何不阻断分享存储层可能暂时拒绝枚举、lstat或删除。若清理失败直接抛到用户界面用户会在当前快照已经安全写好时仍看到“分享失败”而真正的系统分享甚至没有机会打开。生命周期治理应降低风险但不应把非关键旧文件删除升级成当前任务的单点故障。因此枚举失败返回删除数 0单条检查和删除失败被隔离。下一次用户分享还会再次清理系统自身也可能回收 cacheDir。这个“尽力而为”只适用于旧缓存当前正文写入、字节完整性、fsync和 URI 创建仍是必须成功的步骤不能吞掉。如果未来观察到持续清理失败应增加不含路径与文件名的本地诊断计数或者在设置中提供明确的“清理分享缓存”动作。当前阶段不引入遥测也不记录用户文档名称。系统分享仍保持最小权限快照写好后WorkspaceShell 通过系统隐式 Want 发起sendData类型为text/markdownURI 同时写入 stream 参数只授予临时读取权限。接收方得到的是快照 URI不是工作区原文件 URI也没有目录级授权。constshareWant:Want{action:ohos.want.action.sendData,type:text/markdown,uri:shareUri,flags:wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION,parameters:{ability.params.stream:shareUri,ohos.extra.param.key.contentTitle:this.getExportName(md)}};awaitcontext.startAbility(shareWant);缓存治理没有扩大权限、没有新增网络也没有改成更宽泛的text/plain来增加模拟器目标数量。产品继续保持 Markdown 语义兼容接收方缺失仍作为环境缺口记录。鸿蒙 PC 系统分享界面证据下图来自 OhMarkdown 在 HarmonyOS MateBook Pro 2in1 模拟器中打开系统分享界面的真实结果。它证明应用已把当前快照交给系统分享路径系统能够展示目标选择承载面。截图不能证明 32 份上限也不能证明某个兼容应用成功读取 Markdown。缓存数量由 ohosTest 文件枚举断言证明正文正确由readUtf8Document重读证明接收目标成功仍需要安装兼容应用的真机或完整镜像。视觉、存储和系统能力证据必须分开解释。设备测试怎样覆盖误删除新增 ohosTest 在独立测试目录先写入keep.txt随后连续创建 36 个不同名称的 Markdown 分享快照。最终枚举严格匹配的share-*.md必须恰好为 32最新 URI 重读必须得到“正文-35”无关keep.txt必须仍存在。awaitwriteRawText(${shareCacheDirectory}/keep.txt,unrelated cache);letlatestUri:string;for(letindex0;index36;index1){constsequenceindex.toString().padStart(2,0);latestUriawaitcreateMarkdownShareUri(shareCacheDirectory,文章-${sequence}.md,正文-${sequence});}expect(shareNames.length).assertEqual(32);expect(awaitfileIo.access(${shareCacheDirectory}/keep.txt)).assertTrue();expect((awaitreadUtf8Document(latestUri)).content).assertEqual(正文-35);这个用例同时检查上限、当前内容与所有权边界。只检查目录数量会允许错误实现删除无关文件只检查最新文件会允许旧快照无限增长只检查函数返回值则无法证明设备文件系统上的真实结果。首次失败为什么必须保留在记录中首次构建的新测试运行11项其中缓存用例失败期望 32实际 1。其余 10 项通过。失败直接暴露mtime秒与Date.now()毫秒混用。修正单位后重新构建 ohosTest HAP、重新安装并完整运行最终11/11Failure 与 Error 都为 0。保留失败原因能证明测试不是装饰。若报告只留下最终绿色数字后续维护者很难理解时间归一化函数为什么存在可能把它当成“多余兼容代码”删除。设备差异正是原生工程需要测试的内容。最终回归还执行 Playwright44/44、Vite 单 HTML 构建、Debug HAP、ArkTS UnitTestBuild 和 diff check。分享缓存属于 ArkTS/Core File Kit 路径Web 测试不会直接观察文件删除但全量门禁可以证明改动没有破坏编辑、预览和导出。新增的超限文件搜索断言同一次原生质量收口还把工作区搜索的跳过计数变成设备证据。测试在两个正常 Markdown 之外创建一个大小为 4 MiB 加 1 字节的稀疏文件。服务应发现 3 份文档、扫描 2 份、跳过 1 份正常两条搜索结果保持不变。这个断言没有扩大搜索上限而是验证既有安全预算可解释。用户面对超限文件时搜索摘要能区分“没有命中”和“文件未扫描”。压力测试不仅要追求快也要证明限制不会静默改变结论。与系统自动清理的关系应用级策略不取代系统 cacheDir 管理。系统仍可以因空间压力更早删除缓存OhMarkdown 不能把快照当作长期可靠存储。工作区原文与恢复记录分别有自己的安全保存策略分享缓存只是一次用户动作的临时副本。反过来系统可能在空间充足时长期不清理应用的 24 小时和 32 份限制因此仍有价值。两个层级的职责不同系统负责全局资源应用负责理解哪些数据敏感、哪些文件属于自己、当前任务需要保留哪一份。真机仍需验证什么第一安装真实支持text/markdown的接收应用验证系统授予的临时 URI 可以读取完整未保存正文。第二分享面板长时间停留、切换应用、设备休眠再恢复后接收方是否仍能在 24 小时窗口内读取。第三多次同时分享不同文档时旧目标是否依赖超过最近 32 份的快照。第四设备时钟前后跳变、时区变化和不同系统版本的mtime单位是否仍被归一化。第五系统提前回收 cacheDir 时错误提示能否让用户恢复。完成这些任务后才适合把 24 小时写成正式兼容承诺当前只是经过模拟器验证的 Beta 策略。为什么不在本轮引入分享数据库可以为每个快照建立数据库记录、引用计数、接收方回调和后台任务但系统分享目标通常不会向发送应用可靠回报“读取完成”。引入数据库也会增加迁移、损坏、隐私和后台调度成本却不能解决缺少生命周期回执的根本问题。当前 Level 2、D2 边界下基于文件名、修改时间、数量与当前路径的策略已经解决真实 P2不改变工作区模型也不新增长期存储格式。后续只有系统 API 提供可靠完成信号或真实测试证明 24 小时/32 份不足才需要升级设计并记录 ADR。对鸿蒙 PC 产品优势的贡献Markdown 编辑器的本地优先不能只看“没有 INTERNET 权限”。正文还会出现在恢复记录、保存备份、导出文件、图片缓存和分享快照中每种副本都需要独立生命周期。分享缓存收口把安全从传输边界推进到数据留存边界。用户不会因为目录里少了四个临时文件就直接感知功能增加但长期产品的信任正来自这些细节未保存正文能正确分享原工作区 URI 不外露旧快照不会无限增长无关缓存不被误删清理失败又不阻断当前任务。这些约束共同优于“按钮能弹出面板”的功能完成定义。阶段结论提交2b82214完成 G3-10 的分享缓存生命周期小阶段。OhMarkdown 现在只清理严格属于自身的普通share-*.md保留当前 URI最多留存最近 32 份并清理超过 24 小时的快照。时间单位在真实模拟器失败后完成秒/毫秒归一化。最终 Playwright44/44、Debug HAP、ArkTS UnitTestBuild、ohosTest HAP 构建和模拟器11/11通过。内部安全评审 SEC-G3-01 的无界留存问题关闭但 G3 仍为8/10兼容 Markdown 接收方、系统 PDF、鸿蒙 PC 真机、竞品统一测试、真实封闭测试和退出报告尚未完成。阶段没有满足进入 G4 的条件因此继续收口第三阶段而不是提前消费第四阶段范围。