ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Readest 字体同步下载 “Unknown error“ 排查实录:缺失目录根因、错误折叠缺陷与 PR 5700 修复方案

Readest 字体同步下载 “Unknown error“ 排查实录:缺失目录根因、错误折叠缺陷与 PR 5700 修复方案 Readest 字体同步下载 Unknown error 排查实录缺失目录根因、错误折叠缺陷与 PR #5700 修复方案【免费下载链接】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 仓库中 Issue #5675 的排查与修复过程Android 用户 Transfer Queue 中大量字体下载显示Unknown error根因并非网络或鉴权而是原生下载器File::create不创建父目录导致os error 2同时 TransferManager 将非Error类型的原生拒绝折叠为无信息文案。读者将掌握一条完整的排除法定位 → 源码级归因 → 修复落地 → 测试验证的实战链路以及这些修复在当前仓库中的确切代码位置。问题现场Issue #5675 的现象Issue #5675 报告于 Android 0.12.1俄语用户、终身许可。现象非常明确Transfer Queue 面板显示Completed: 0, Failed: 1616 个字体下载全部失败错误标签为Unknown error俄语环境显示Неизвестная ошибка同时弹出 toastFailed to download file: font。这批失败并非随机网络抖动——失败数量与待下载字体数量完全一致且全部落在同一个错误文案上。这意味着问题大概率出在某一个共性的系统环节而不是逐文件独立的网络错误。排除法定位先证明不该失败的地方都没失败排查的第一步是把失败责任逐层剥离文档作者给出了三条已证实而非猜测的证据链元数据与 manifest 确实到达了接收设备。fontAdapter.unpackRow在row.manifest_jsonb缺失时返回null见 adapters 助手 中的singleFileFilenameFromManifest而 replicaPullAndApply.ts 对空 manifest 直接提前返回。因此队列里出现了一个下载任务本身就证明发布端已完成uploadReplicaFilemanifest 只在replicaTransferIntegration.handleReplicaUpload上传完成之后才提交。/api/storage/download接口调用成功。所有 JS 侧的失败都会抛出带真实消息的ErrorfetchWithAuthutils/fetch.ts会重新抛出服务端返回的error字符串getUserID为 null 时抛Error(Not authenticated)缺少 URL 时抛Error(No download URL available)webDownload失败抛Error(...)缺失files行 404 时抛Error(File not found)。这些文案一个都没有出现。结论拒绝必然来自invoke(download_file)原生调用。src-tauri/src/transfer_file.rs 中impl Serialize for Error将错误序列化为纯 JS 字符串serializer.serialize_str(self.to_string())而 transferManager.ts 第 502 行的错误折叠逻辑error instanceof Error ? error.message : _(Unknown error)会把任何非Error类型的拒绝值统一映射为字面量Unknown error随后在第 551 行存入 transfer 状态。仓库中的 transfer-manager.test.ts 用一次临时的 vitest 验证印证了该推断downloadReplicaFile以字符串拒绝时transfer.error Unknown error以new Error(File not found)拒绝时消息被完整保留。根因File::create不创建父目录 mkdir 与 id minting 被熔合Rust 侧原生下载器只写文件不建目录transfer_file.rs 单线程路径与 第 268 行 多线程分段路径都直接调用File::create(file_path)。File::create的语义是打开或创建指定文件它不会递归创建父目录——当Fonts/bundleDir/不存在时立即以No such file or directory (os error 2)失败。JS 侧mkdir 被熔合进了 id minting真正不寻常的部分在于目录为什么会在设备上凭空消失答案在 useReplicaPull.ts 的createBundleDircreateBundleDir: async () { const id uniqueId(); // 铸造一个新的 bundleDir id await service.createDir(id, config.baseDir!, true); // 同时创建磁盘目录 return id; },它把生成唯一 id和创建目录两个职责融合在同一个函数里。而 replicaPullAndApply.ts 的applyRow在local分支本地已存在该 contentId 的记录中刻意不调用createBundleDir——原因在注释里写得很清楚如果为一条已有记录铸造新 id会让旧二进制文件变成孤儿并且每次 pull 都会触发全量重下载。于是作为附带损伤mkdir 也被一并跳过了。结果就是local分支的下载路径包括从 localStorage 重放的持久化失败队列、Retry All 重试、以及目录丢失后的local记录全部在目录可能不存在的前提下写入文件。没有任何代码在下载前重新建立目录必须存在这个不变量。为什么只有记录存活、目录死亡的设备中招路径解析分裂要理解这个问题为何只在部分设备上爆发需要看清记录record与文件file住在不同的地方字体记录本身settings.json保存在AppConfig应用内部目录中字体二进制文件保存在Fonts/目录下而Fonts/跟随用户自定义的customRootDirgetPathResolver 对Fonts/Books/Images/Dictionaries都应用 custom root唯独不作用于 Settings。因此只要用户修改了 customRootDir或清除了外部存储就会出现文档中描述的极端不对称每一条记录都存活记录在内部存储里安然无恙每一个 bundle 目录却全部消失目录跟着旧 root 一起没了。这批记录比目录长寿的设备正是Unknown error的重灾区。全新设备反而完全正常——因为新设备的记录和目录是同时创建的。修复方案PR #5700fix/replica-download-bundle-dirPR #5700 共 2 个 commit修复策略是在下载时兜底建目录而不是在applyRow里补 mkdirCommit 1下载前确保 bundle 目录存在修复点位于 appService.ts 的downloadReplicaFile在把绝对路径交给原生下载器之前先建目录async downloadReplicaFile(kind, replicaId, filename, lfp, base, onProgress?) { // The native downloader writes with File::create, which does not create // parent directories, so a missing bundle dir fails as an opaque // No such file or directory (os error 2) (issue #5675). const bundleDir getDirPath(lfp); if (bundleDir) { await this.fs.createDir(bundleDir, base, true); } // Resolve the relative bundleDir/filename lfp against the // replicas base dir before downloading. const dst await this.resolveFilePath(lfp, base); return CloudSvc.downloadReplicaFileFromCloud(this, { kind, replicaId, filename, dst, onProgress, }); }关键设计考量getDirPathutils/path.ts从 lfp 中剥离出bundleDir部分createDir是递归的目录已存在时是 no-op无额外开销。放在下载时而非 applyRow因此持久化队列重放persisted-queue replay和 Retry All 也被一并覆盖——这正是文档强调at the DOWNLOAD, not in applyRow的原因。这与既有防御保持了一致cloudService.downloadBook与downloadBookCovers本就先createDir再下载此次修复补齐了 replica 下载路径上缺失的同款护栏。Commit 2replica 传输默认后台化失败路径也静默第二个 commit 修复了一个配套缺陷replica 传输现在默认isBackground: true并且失败路径同样遵守该标记。此前只有成功路径遵守isBackground失败路径未遵守——这正是 Issue #5675 中16 个字体 16 条Failed to download filetoast刷屏的来源。修复后见 transferManager.ts后台 replica 的失败不再逐文件弹 toast但失败状态仍会写入 transferTransfer Queue 面板依旧可以读到错误信息。测试验证目录先于下载创建仓库为这次修复补充了完整的单测位于 app-service.test.ts 的downloadReplicaFiledescribe 块覆盖五个场景测试用例断言下载前创建 bundle 目录createDir以(v04c1uy, Fonts, true)被调用创建发生在下载之前createDir的调用序 downloadReplicaFileFromCloud的调用序嵌套路径创建完整父链(bundle-1/assets, Dictionaries, true)无 bundle 目录的扁平旧路径不建目录createDir未被调用仍下载到解析后的绝对路径dst包含v04c1uy/Georgia.ttf此外 transfer-manager.test.ts 验证了后台化语义replica 传输不弹 info toast当downloadReplicaFile以原生字符串No such file or directory (os error 2)拒绝正是真实 os-error-2 的形态时重试耗尽后不再派发错误 toast。设备 A/B 证明同一台设备目录有无决定成败文档记录了在一台 Xiaomi 设备customRootDir/storage/emulated/0/Books上的 A/B 对照验证目录缺失failed→ 重试 3 次 →Unknown errorconsole 输出os error 2目录存在completed下载字节数 379588 完全一致Georgia:loadedfont-face注入成功。同时完整跑通了全链路导入import→ 上传upload→ manifest 提交 → pull → 下载download→ 字体挂载mount。这组证据将根因从推断升级为实锤。尚未修复的次生缺陷Unknown error文案折叠PR #5700 明确标注了一个仍未修复的 follow-up原生错误在 UI 中被折叠成无信息的Unknown error。问题位于 transferManager.tsconst errorMessage error instanceof Error ? error.message : _(Unknown error);由于 Rust 侧将错误序列化为纯字符串transfer_file.rs 的serialize_str而此处只识别Error实例于是四类原生失败模式在 UI 中完全不可区分且不被记录Forbiddenfs-scope 越权相关历史见ensure_path_allowed与 in-place-delete 的 fs-scope 演进Request网络 / TLS 层失败HttpErrorCode403/404R2 云端返回Ioos-error-2目录缺失/ os-error-28磁盘满。文档给出的修复方向是在executeTransfer中先接受字符串拒绝即typeof error string ? error : ...让原生错误文案直达 UI。同时指出downloadFile目前只console.error使得这类报告完全无法诊断——这是比目录缺失更需要优先解决的真实缺陷。Fleet 佐证与更多暴露出来的未处理路径Sentryreadest org数据将问题从个例升级为平台级现象搜索message:*Readest/Fonts*可看到大量failed to get metadata of path: .../Readest/Fonts/bundleDir/file ... No such file or directory (os error 2)覆盖Android、iOS、Windows三个平台版本跨度 0.11.18 → 0.12.1且非特定用户——占位记录存在而二进制从未落盘。另有failed to create directory ... No space left on device (os error 28)来自importFont属于未处理路径。还发现一个泄漏的 Android SAF doc-id 被当作字体文件名primary%3AFonts%2FTaiwanPearl-Regular.ttf——importFont未对fileobj.name执行makeSafeFilename。仍待确定与附带发现四种原生错误中究竟是哪一种击中了本次报告者仍无法确定。Sentry 中一条 Android 0.12.1 路径为/storage/emulated/0/Books/Readest/Fonts/...custom root 位于外部存储对那一子集而言File::create的 EACCES权限拒绝同样是可疑来源。文档指出需要错误透出修复上线 用户重新上报或设备端复现才能定论。附带发现与本次问题无关但真实存在useCustomFontStore 的findByContentId不过滤deletedAt而持久化 fallbackfindFontByContentId会过滤——被软删除的本地字体会遮蔽存活的远端行导致设备 A 上删除后重新导入的字体现在设备 B 上无法复活。设备探测记录用于验证的 Xiaomi fuxi368b0948运行的是 RELEASE 0.12.1——webview_devtools_remote_pidsocket 存在但不提供 CDPrun-as被拒包不可调试、无 root/sdcard/Android/data/com.bilingify.readest/files为空。要在设备上做深层检查需要pnpm dev-androiddevtools 构建。总结Issue #5675 是一次教科书级的跨语言边界 bugJS 侧的错误折叠把 Rust 原生的字符串拒绝吞成Unknown error而真正的根因——File::create不建父目录、mkdir 与 id minting 熔合导致的local分支目录缺失、以及 customRootDir 让记录与文件分裂存储——环环相扣。PR #5700 在下载入口处补齐了createDir(getDirPath(lfp), base, true)护栏并将 replica 传输彻底后台化配合五组单测锁死行为。对于仍困扰用户的错误不可诊断问题仓库已给出明确的下一步修复方向在executeTransfer接受字符串拒绝值得任何关注该仓库的开发者跟进。【免费下载链接】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创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表