Joplin 同步目标快照(Sync Target Snapshot)v2 格式解析:从 note2 条目文件到同步版本迁移机制
Joplin 同步目标快照Sync Target Snapshotv2 格式解析从 note2 条目文件到同步版本迁移机制【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplinJoplin 通过统一的“文件系统式”同步目标模型将笔记本、笔记、标签、资源等所有数据以条目文件形式存储在各同步目标Nextcloud、WebDAV、Joplin Cloud、本地目录等上。本文以仓库测试夹具packages/app-cli/tests/support/syncTargetSnapshots/2/normal/edd3cb394ada4d389c01c2bdca09b3ef.mdv2 快照中的note2条目为入口完整解析 Joplin 同步目标快照的目录结构、条目文件格式、字段语义并深入同步迁移测试与版本升级机制读者可据此独立解读任意快照条目、理解info.json与.sync/version.txt的演进关系以及快照如何被 Joplin 的迁移测试自动消费。一、快照体系概览测试夹具中的“同步目标黄金样本”在 Joplin 仓库中packages/app-cli/tests/support/syncTargetSnapshots/目录保存了多个历史版本的同步目标快照。每个快照本质上是一份“当时真实同步到目标端后形成的文件集合”用于保证同步格式升级时数据不被破坏。目录按同步版本号1/2/3与是否启用端到端加密normal/e2ee双维度组织packages/app-cli/tests/support/syncTargetSnapshots/ ├── 1/ │ ├── normal/ # v1 未加密快照19 个条目文件 │ └── e2ee/ # v1 E2EE 快照20 个条目文件 ├── 2/ │ ├── normal/ # 本文主题快照含 info.json 与 locks/ │ │ ├── info.json # {version:2} │ │ ├── locks/ # 锁目录快照中为空 │ │ └── *.md # 20 个条目文件 │ └── e2ee/ └── 3/ ├── normal/ └── e2ee/每个版本目录内都有一个info.json内容形如{version:2}它直接宣告该同步目标的格式版本——这正是MigrationHandler.fetchSyncTargetInfo()见 MigrationHandler.ts读取并解析的文件。值得注意v1 快照目录中没有info.json只有.sync/version.txt这是新旧版本标识机制的分水岭后文详述。v2 的normal/快照中同时出现locks/目录与源码中Dirnames.Locks locks见 utils/types.ts一一对应。这些目录由 v1→v2 迁移逻辑显式创建见下文迁移章节是版本快照之间“可被测试断言”的结构化差异。二、条目文件格式以 note2 为例逐字段拆解快照中的每个条目对应一个.md文件文件名即条目的全局唯一 ID。本文核心文档edd3cb394ada4d389c01c2bdca09b3ef.md是一个位于folder1/subFolder2/下的普通笔记note2全文如下note2 id: edd3cb394ada4d389c01c2bdca09b3ef parent_id: 2fa39884ba3b47a489dae93dc20021f2 created_time: 2020-07-25T10:55:18.422Z updated_time: 2020-07-25T10:55:18.422Z is_conflict: 0 latitude: 0.00000000 longitude: 0.00000000 altitude: 0.0000 author: source_url: is_todo: 0 todo_due: 0 todo_completed: 0 source: joplin source_application: net.cozic.joplintest-cli application_data: order: 1595674518422 user_created_time: 2020-07-25T10:55:18.422Z user_updated_time: 2020-07-25T10:55:18.422Z encryption_cipher_text: encryption_applied: 0 markup_language: 1 is_shared: 0 type_: 1该格式可以归纳为三个逻辑段1. 标题行与正文区第一行为条目标题本文件为note2若条目有正文紧跟一个空行后为 Markdown 正文note2正文为空因此空行后直接进入属性块对照同目录下的note12a914b3fb8fb43819b976eb4e5be80e3.md可看到正文区的实际形态note1 [](https://link.gitcode.com/i/1444b77a293a8d7ae3e3fd4494221153) id: 2a914b3fb8fb43819b976eb4e5be80e3 ...其中[](https://link.gitcode.com/i/1444b77a293a8d7ae3e3fd4494221153)是 Joplin 的资源引用语法:/后跟资源 ID指向同目录下的资源条目6f60ca35b0e4423fb49f9e097449fd99.md。资源条目文件如 006a89df4de64a22b4b1fa71f87fd258.md以photo.jpg为标题额外携带mime: image/jpeg、file_extension: jpg、size: 2720等字段。note1测试脚本正是通过markdownUtils.extractImageUrls(note.body)提取正文图片 URL 并加载对应资源来验证引用完整性见 syncTargetUtils.ts。2. 属性块以key: value形式序列化的字段空行之后的每一行均为字段名: 值值可能为空如author:。全部字段取自数据库表列核心字段语义如下字段语义备注id条目全局唯一 ID与文件名一致由uuid.create生成见 BaseModel.tsparent_id父目录 ID笔记→所属文件夹文件夹→父文件夹根文件夹为空created_time/updated_time条目创建/更新时间ISO 8601 UTCuser_created_time/user_updated_time用户可见时间通常与系统时间一致is_conflict是否为冲突笔记0/1latitude/longitude/altitude地理位置元数据未使用时为0author/source_url来源元数据剪藏等场景使用is_todo/todo_due/todo_completed待办标记及截止/完成时间普通笔记为0source来源类型如joplinsource_application创建条目的客户端标识本快照为net.cozic.joplintest-cliorder排序权重毫秒时间戳如1595674518422application_data应用扩展数据本快照为空encryption_cipher_textE2EE 密文未加密快照为空encryption_applied是否已加密0/1markup_language标记语言1 Markdownis_shared是否共享服务端功能0/1type_条目类型枚举见下节本条为1Note3.type_条目的类型指纹type_是序列化时最重要的判别字段其枚举定义位于 BaseModel.tstype_ 值常量含义快照中示例1ModelType.Note笔记edd3cb39…本文档2ModelType.Folder文件夹/笔记本c4e45cad…folder1、2fa39884…subFolder24ModelType.Resource附件资源006a89df…photo.jpg5ModelType.Tag标签6cb91bb2…tag16ModelType.NoteTag笔记-标签关联56748647…note_idtag_id字段不同type_的条目文件携带各自的专属字段例如标签条目6cb91bb296ee458589eea0256ada06fa.md没有正文与正文区第一行tag1即标签名NoteTag关联条目567486477f4249d38feadf6c5ec6e03d.md则以note_id、tag_id两个外键字段表达多对多关系。三、快照从何而来测试数据生成器快照并非手工编写而是由测试工具syncTargetUtils.ts在 CI 或本地自动生成。其核心逻辑main()大致为初始化 Node 端 shimsharp、nodeSqlite与测试数据库、同步器调用createTestData(testData)按预定结构写入数据若为e2ee类型则启用加密并加载主密钥setEncryptionEnabled(true)、loadEncryptionMasterKey()启动同步器执行完整同步将同步目录整体拷贝到${snapshotBaseDir}/${syncVersion}/${syncTargetType}即生成当前版本快照。testData结构syncTargetUtils.ts精确对应快照中的文件夹与笔记凡名称含folder的节点创建为文件夹Folder.save其余创建为笔记Note.save并可附加资源与标签folder1/ ├── subFolder1/ ├── subFolder2/ │ ├── note1 # 附 photo.jpg 资源 tag1 │ └── note2 # ← 本文关联文档 ├── note3 # tag1 tag2 └── note4 # tag2 folder2/ folder3/ └── note5 # photo.jpg tag2recurseStructsyncTargetUtils.ts递归遍历该树parent_id沿递归链传递——这解释了为何note2的parent_id指向subFolder22fa39884…而subFolder2的parent_id又指向folder1c4e45cad…。photo.jpg来自shim.attachFileToNote(note, ${supportDir}/photo.jpg)。测试代码注释中还给出了手动重建快照的命令将test-utils中syncTargetName_设为filesystem后运行node tests/support/createSyncTargetSnapshot.js normal及e2ee见 synchronizer_MigrationHandler.test.ts。与之对称的校验函数checkTestDatasyncTargetUtils.ts在同步/迁移完成后反向验证数据完整性按标题加载文件夹与笔记、校验父文件夹、提取资源 URL 并加载资源、校验标签关联——任何一环缺失都会抛出错误。四、快照的消费者同步版本迁移测试快照存在的根本目的是服务同步版本迁移测试。synchronizer_MigrationHandler.test.tssynchronizer_MigrationHandler.test.ts的测试思路是“取版本 n 的快照升级到 n1验证数据未被改动”deploySyncTargetSnapshot(normal, migrationVersion - 1)将对应旧版快照拷贝为当前同步目录syncTargetUtils.ts先fs.remove(syncDir)再fs.copy(sourceDir, syncDir)通过fetchSyncInfo(fileApi())断言目标版本确实为migrationVersion - 1Setting.setConstant(syncVersion, migrationVersion)后调用migrationHandler().upgrade(migrationVersion)执行迁移再次fetchSyncInfo断言版本已升到migrationVersion校验目录结构断言见migrationTests例如 v2/v3 均需存在.resource、locks、temp目录与info.json文件且.sync/version.txt内容为2到达最大版本后运行完整同步并用checkTestData(testData)校验数据未被迁移改动最后switchClient(2)切换到第二个客户端再同步一次模拟多客户端场景。同样的流程对e2ee快照执行synchronizer_MigrationHandler.test.ts迁移后需用测试主密钥密码123456加载密钥并启动decryptionWorker解密数据再执行数据校验。测试还预留了扩展点新增迁移时需在MigrationHandler.ts的migrations数组追加迁移函数、在Setting.syncVersion提升版本号、并补充migrationTests断言MigrationHandler.ts。五、版本标识的演进info.json 与 .sync/version.txt快照 v1、v2、v3 之间的差异正是同步版本机制的演进史三者都体现在 migrations/ 目录下v1→v2migration 2migrations/2.ts 一次性完成三项工作写入.sync/version.txt内容2——注意新格式下版本号已改存于根目录info.json但必须保留该旧文件否则旧客户端会自动重建它并误判目标版本为 1创建locks/目录Dirnames.Locks创建temp/目录Dirnames.Temp。这解释了 v2 快照中同时存在info.json与locks/的结构事实。初版migration 1migrations/1.ts 创建.resource、.sync、.lock三个隐藏目录并写入.sync/version.txt 1——即最古老的版本标识方式也是fetchSyncTargetInfo中“无info.json时回退读取.sync/version.txt”逻辑的来源MigrationHandler.ts。v2→v3migration 3migrations/3.ts 不再改动目录结构而是将本地缓存的SyncInfosyncInfo.version 3通过uploadSyncInfo写入info.json。版本探测与防错fetchSyncInfosyncInfoUtils.ts完整呈现了版本判定优先级存在info.json→ 解析 JSON缺失version字段直接抛错无info.json但有.sync/version.txt→ 视为 v1需升级两者皆无 → 触发 fail-safe 检查syncInfoUtils.ts仅在初始同步syncedItems.length 0时放行否则抛出failSafe错误防止空/损坏目标造成数据丢失。而checkCanSyncMigrationHandler.ts则负责双端版本比对目标版本高于客户端支持版本抛outdatedClient低于则抛outdatedSyncTarget。六、info.json 的完整形态SyncInfo 序列化快照中 v2 的info.json只是最简形态{version:2}但实际运行时该文件由SyncInfo类序列化而来。serialize()使用带缩进的 JSON 输出syncInfoUtils.ts完整字段包括toObject()字段说明version同步目标版本号e2ee是否启用端到端加密布尔值 时间戳activeMasterKeyId当前活动主密钥 IDmasterKeys主密钥列表上传时通过filterSyncInfo剥离content/checksum等敏感属性noteLockKey笔记锁密钥ppk端到端加密公私钥对过滤时截断显示appMinVersion可同步的最小应用版本仓库中为3.7.0级常量见 syncInfoUtils.tsrevisionServiceEnabled/revisionServiceTtlDays修订历史服务开关与保留天数默认 90 天其中e2ee、activeMasterKeyId等参数带updatedTime时间戳用于多客户端冲突仲裁migrateLocalSyncInfo将来源不明的时间戳置 0从而让“后设置的值优先”避免旧客户端用旧值覆盖新客户端刚开启的加密syncInfoUtils.ts。这也是为什么e2ee/快照的条目文件会带有encryption_applied: 1与密文字段而normal/快照如本文档中这些字段为空。七、实战如何用快照体系验证与调试同步快速定位某个条目的归属拿到任意快照条目文件后可按type_判断类型、按parent_id沿树向上回溯。例如note2parent_id 2fa39884…subFolder2→ 其parent_id c4e45cad…folder1→parent_id为空即根文件夹。由此还原出完整层级folder1/subFolder2/note2。核对同步目标版本cat packages/app-cli/tests/support/syncTargetSnapshots/2/normal/info.json输出{version:2}若该文件缺失则应到.sync/version.txt中寻找旧式版本号——这两种文件恰好构成fetchSyncInfo的两条读取路径。手动重放迁移验证可参照测试流程在本地以filesystem作为同步目标先按 synchronizer_MigrationHandler.test.ts 的注释生成快照再编写或复用deploySyncTargetSnapshot将旧版快照铺入同步目录随后以新版syncVersion启动同步器观察升级结果与checkTestData校验输出。仓库中deploySyncTargetSnapshot(normal, 1)、(e2ee, 1)的调用样例见 synchronizer_MigrationHandler.test.ts。理解快照与客户端代码的版本约束快照目录1/2/3与迁移数组migrations [null, migration1, migration2, migration3]一一对应最大同步版本由migrationTests的键集合推导maxSyncVersion Number(Object.keys(migrationTests).sort().pop())见 synchronizer_MigrationHandler.test.ts。新增格式改动时需要同时满足“迁移逻辑、版本号、快照、断言”四者同步更新这正是该测试基建保证同步格式向后兼容的工程约束。八、小结一份看似平淡的note2快照条目实际串联起 Joplin 同步体系的完整链路条目文件是“标题 正文 属性块”三段式序列化type_字段驱动多类型模型笔记/文件夹/资源/标签/关联info.json与.sync/version.txt承载版本演进与旧客户端兼容syncTargetSnapshots目录为同步迁移测试提供黄金样本而syncTargetUtils、MigrationHandler、syncInfoUtils三个模块则分别负责快照的生成、迁移执行与版本探测。理解这份格式不仅能读懂任意快照内容也能在排查同步版本冲突、数据完整性校验与多客户端兼容问题时快速定位根因。【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考