资讯动态

Readest 就地阅读模式误删原始文件的根因剖析与修复实践(PR 4696)

发布时间:2026/9/20 12:38:07 来源:尧图企业网站定制
桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载导读本文深入剖析 Readestapps/readest-app中一个曾被用户数据安全事件触发的经典缺陷当用户以 Read books in place就地阅读方式导入书籍后在应用内删除该书Readest 会永久删除用户磁盘上的原始源文件且不经过系统回收站、无法恢复。文章将完整还原问题的用户报告、根因调用链deleteBook→resolveBookContentSource→fs.removeFile、测试通过 ≠ 行为正确的工程陷阱以及 PR #4696 的修复策略与回归测试证据并给出修复后不同删除动作local/both/purge的实际行为矩阵。读完本文你将理解 Readest 中受管文件与外部文件的边界划分逻辑以及如何在删除类功能中避免误伤用户自有数据。问题场景重现一次不可逆的数据丢失根据 in-place-delete-wiped-originals.md 记录的原始用户报告Windows 平台、v0.11.12 版本用户通过 Import From Directory 以Read books in place就地阅读模式导入一个文件夹之后在 Readest 应用内删除这些书籍结果 Readest永久删除了用户磁盘上的原始本地文件——连回收站Recycle Bin都没有经过由于云同步尚未上传这些文件状态为 Book File Not Uploaded用户没有任何副本文件不可恢复。这里的核心矛盾在于在用户的心智模型中删除书架条目只是从阅读应用中移除一本书而 Readest 的实际行为却是删除了磁盘上属于用户自己的源文件。根因剖析删除调用链中的无差别清理入口deleteBook按动作类型分支清理删除书籍的入口函数是 src/services/cloudService.ts 中的deleteBook(fs, book, deleteAction)。删除动作由DeleteAction联合类型定义见 src/types/system.tsexport type DeleteAction cloud | local | both | purge;动作语义cloud仅删除云端已上传的文件不动本地local仅删除本地内容保留云端both本地与云端同时删除purge彻底清除本地文件、全部 sidecar 元数据并排队删除云端在旧版本实现中对于local/both/purge三类动作deleteBook都会调用resolveBookContentSource解析书籍内容的来源并在source.kind external即设置了book.filePath、base为None时无条件执行fs.removeFile(source.path, source.base)——这正是灾难的源头。来源判定resolveBookContentSource如何区分文件归属来源解析逻辑位于 src/services/bookContent.ts其类型定义清晰划分了五种来源export type BookContentSource | { kind: managed; path: string; base: Books; legacy?: boolean } | { kind: external; path: string; base: None } | { kind: url; path: string; base: None; fetcher?: typeof fetch } | { kind: stream; path: string; base: None; scheme: pse } | { kind: feed; path: string; base: None } | { kind: missing };关键区分在于managed与externalmanaged文件位于 Readest 自己的Books/hash/目录下是导入时由应用复制的副本属于 Readest 生成的数据删除书架条目时清理它是合理的externalbase为None文件位于用户自己控制的位置即book.filePath指向的路径它可能是就地阅读导入的原始文件也可能是 Open with Readest 打开的外部文件Android 上还会是content://URI属于用户自己的数据Readest 无权删除。resolveBookContentSourcebookContent.ts的判定顺序为优先返回已存在的受管副本 → ABS 电子书走url→filePath存在含content://URI则返回external→url/流/订阅源 → 兜底查找 legacy 受管副本或missing。book.filePath的来源就地导入与瞬时打开book.filePath何时被写入见 src/services/bookService.ts 的importBook} else if (transient || inPlace) { // transient: source file is loaded directly, never persisted in Books/. // inPlace: source file is inside the users library root and we read it // there directly instead of duplicating it under Books/hash/. book.filePath file; ... }也就是说只要导入是transient瞬时打开预览或inPlace就地阅读模式book.filePath就会被设置为用户原始文件路径。此时 Books 目录下并没有 Readest 自己的副本——书的内容直接从用户的原始文件读取。这从代码结构上印证了文档中的结论Readest never copied it。被删除方与拥有方的错位把以上三条拼起来旧版本缺陷的形成机制就完全清晰了就地/瞬时导入 →book.filePath指向用户原始文件 → 来源判定为externaldeleteBook对local/both/purge一律走到source.kind external分支无条件fs.removeFile(source.path, None)→删掉的是用户自己的源文件。应用拥有的数据Books/hash/副本、cover.png等 sidecar与用户拥有的数据外部源文件被混为一谈、同等清理这是整个缺陷的本质。最隐蔽的陷阱这不是意外而是被有意编码并测试过的行为文档特别强调了一个反直觉的事实这也是本案例最有价值的工程教训This was NOT an accidental bug — it wasdeliberately coded AND tested.在修复之前cloud-service.test.ts中存在一整个in-place (book.filePath set)的describe测试块其断言是源文件应当被删除测试注释中还为该行为辩护理由是与删除普通书籍时删除Books/hash/title.epub保持对称symmetric。也就是说该行为通过了既有测试该行为有测试维护者在注释中给出的合理性论证但维护者在收到用户报告后推翻了这一决策。这带来的教训是测试通过只能证明代码符合当前断言的预期不能证明断言本身的正确性。当产品语义删除书架条目与实现假设删除条目 删除全部相关文件发生冲突时必须回到用户视角重新审视被测行为是否合理。所以文档直言Dont assume tested intended; the maintainer reversed the decision.修复方案PR #4696划定 Readest 的删除边界修复的核心原则可以用一句话概括该原则已直接写入deleteBook的实现注释见 cloudService.tsOnly remove files Readest itself created. Deleting a book from Readest must NEVER remove that source file.具体改动如下删除external分支deleteBook不再对external来源的文件调用fs.removeFile只删除managed来源仅在source.kind managed且deleteAction ! purge时删除单个受管书文件purge会整目录清理无需逐文件删除purge的动作收窄purge现在只删除 Readest 自己生成的Books/hash/整个 sidecar 目录包含受管书文件、cover.png、以及config.json阅读进度/笔记/书签和nav.json这正是 issue #4615 引入purge语义的原因就地阅读书籍的外部源文件始终不动补充清理purge同时删除Cache/tts-cache/hash的 TTS 音频缓存避免备份与同步误收录非purge时保留它以便重新下载后暖缓存续读非purge时还会清理 Audiobookshelf 的离线轨道目录#6256修正误导性 JSDoc修复了 ImportFromFolderDialog.tsx 中readInPlace选项把破坏性行为写成预期行为的注释——文档明确点名了这一点Also fixed the misleading JSDoc ... that documented the destructive behavior as intended.修复后的状态字段更新逻辑cloudService.tslocal/purge仅将downloadedAt置空墓碑deletedAt由调用方handleBookDelete负责设置并排队云端删除both则同时清除deletedAt、downloadedAt、coverDownloadedAt。云端分支cloudService.ts仍只在uploadedAt存在时删除远程书文件与封面上传件。修复后的行为矩阵以当前仓库源码为准修复后不同删除动作对三类文件的实际影响如下删除动作用户原始文件externalbase: None受管副本Books/hash/title.epubmanagedsidecar 目录Books/hash/含cover.png、config.json、nav.json云端文件cloud不动不动不动删除需已上传local不动删除不动不动both不动删除仅删cover.png删除需已上传purge不动随目录一起删除整目录删除含进度/笔记/书签删除需已上传对照表可见核心边界无论哪个动作用户自己的源文件都受到绝对保护Readest 只清理自己生成、自己拥有生命周期的数据。回归测试把保护外部源文件固化为契约修复不是口头承诺而是被测试固化的行为契约。相关测试位于 src/tests/services/cloud-service.test.ts 的in-place (book.filePath set)描述块测试注释明确写道Readest never copied it. Deleting such a book from Readest must NEVER remove that source file; only the app-generated sidecars ... are ours to delete. (Regression: in-place delete used to wipe the users originals.)关键断言逐条对应修复语义local动作不得调用removeFile(/Users/me/Library/sample.epub, None)测试 L339-L347local动作对就地书籍一个文件都不删无受管副本可删外部源又受保护仅清除downloadedAtL349-L367源文件缺失时不抛异常L369-L379both动作删除cover.pngsidecar但绝不删除外部源文件L381-L394purge动作整目录清除 sidecar但外部源文件绝不动L249-L265。这些测试同时覆盖了local、both、purge三种本地删除动作确保未来任何重构都不会再次触碰用户原始文件。遗留问题与工程启示文档最后指出删除流程仍有两项已知不足记录于 support thread未在 PR #4696 中修复删除流程缺乏警告/免责声明用户删除书籍前没有提示将同时移除本地文件用户难以预判后果删除不走操作系统回收站无论删除哪类文件都直接物理删除无法从回收站恢复。从该案例可以提炼出适用于任何文件管理类软件的通用原则以所有权而非路径划分删除边界应用只应删除自己创建、自己管理生命周期的数据对用户自有文件宁可留下孤儿引用也不可越权删除测试断言也要接受产品语义审查测试通过 ≠ 行为正确被注释合理化的破坏性行为尤其需要警惕不可逆操作必须多层设防警告提示、回收站、备份校验缺一不可本案例中云同步尚未上传Book File Not Uploaded导致连远端副本都不存在进一步放大了损失。对于 Readest 用户而言修复后的行为可放心使用以 Read books in place 或 Open with Readest 打开的书在应用内删除书架条目不会再伤害磁盘上的原始文件。赞分享桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载相关推荐Readest 阅读器批注修复实录Annotate 后新批注不可见5987/5957的根因分析与就地编辑器方案Readest 阅读器批注修复实录Annotate 后新批注不可见 5987/ 5957的根因分析与就地编辑器方案 本篇技术指南围绕 Readest 阅读桌面应用跨平台前端Readest 跨平台阅读器 Bug 修复模式与策略从根因分类到源码级调试实战Readest 跨平台阅读器 Bug 修复模式与策略从根因分类到源码级调试实战 导读 本文以 Readest 开源仓库中沉淀的《Bug Fixing Patt桌面应用跨平台前端Readest 自动导入按子文件夹分组失效问题修复解析Issue 5423 的根因、修复与工程实践Readest 自动导入按子文件夹分组失效问题修复解析Issue 5423 的根因、修复与工程实践 导读 本文基于 Readest 仓库中的修复记忆文档桌面应用跨平台前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价