首页
/ Joplin 同步目标快照(Sync Target Snapshot)v2 格式解析:从 note2 条目文件到同步版本迁移机制

Joplin 同步目标快照(Sync Target Snapshot)v2 格式解析:从 note2 条目文件到同步版本迁移机制

2026-09-09 19:10:12作者:凤尚柏Louis

Joplin 通过统一的“文件系统式”同步目标模型,将笔记本、笔记、标签、资源等所有数据以条目文件形式存储在各同步目标(Nextcloud、WebDAV、Joplin Cloud、本地目录等)上。本文以仓库测试夹具 packages/app-cli/tests/support/syncTargetSnapshots/2/normal/edd3cb394ada4d389c01c2bdca09b3ef.md(v2 快照中的 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

[![photo.jpg](https://gitcode.com/GitHub_Trending/jo/joplin/blob/71d4b09d48d78d1dc71d1d04dcea2f64d3c0aaee/packages/app-cli/tests/support/syncTargetSnapshots/2/normal/.resource/6f60ca35b0e4423fb49f9e097449fd99?utm_source=gitcode_repo_files)](https://gitcode.com/GitHub_Trending/jo/joplin?utm_source=gitcode_repo_files)

id: 2a914b3fb8fb43819b976eb4e5be80e3
...

其中 [![photo.jpg](https://gitcode.com/GitHub_Trending/jo/joplin/blob/71d4b09d48d78d1dc71d1d04dcea2f64d3c0aaee/packages/app-cli/tests/support/syncTargetSnapshots/2/normal/.resource/6f60ca35b0e4423fb49f9e097449fd99?utm_source=gitcode_repo_files)](https://gitcode.com/GitHub_Trending/jo/joplin?utm_source=gitcode_repo_files) 是 Joplin 的资源引用语法::/ 后跟资源 ID,指向同目录下的资源条目 6f60ca35b0e4423fb49f9e097449fd99.md。资源条目文件(如 006a89df4de64a22b4b1fa71f87fd258.md)以 photo.jpg 为标题,额外携带 mime: image/jpegfile_extension: jpgsize: 2720 等字段。note1 测试脚本正是通过 markdownUtils.extractImageUrls(note.body) 提取正文图片 URL 并加载对应资源来验证引用完整性(见 syncTargetUtils.ts)。

2. 属性块:以 key: value 形式序列化的字段

空行之后的每一行均为 字段名: 值,值可能为空(如 author:)。全部字段取自数据库表列,核心字段语义如下:

字段 语义 备注
id 条目全局唯一 ID 与文件名一致,由 uuid.create 生成(见 BaseModel.ts
parent_id 父目录 ID(笔记→所属文件夹;文件夹→父文件夹) 根文件夹为空
created_time / updated_time 条目创建/更新时间(ISO 8601 UTC)
user_created_time / user_updated_time 用户可见时间(通常与系统时间一致)
is_conflict 是否为冲突笔记 0/1
latitude / longitude / altitude 地理位置元数据 未使用时为 0
author / source_url 来源元数据 剪藏等场景使用
is_todo / todo_due / todo_completed 待办标记及截止/完成时间 普通笔记为 0
source 来源类型(如 joplin
source_application 创建条目的客户端标识 本快照为 net.cozic.joplintest-cli
order 排序权重(毫秒时间戳) 1595674518422
application_data 应用扩展数据 本快照为空
encryption_cipher_text E2EE 密文 未加密快照为空
encryption_applied 是否已加密 0/1
markup_language 标记语言 1 = Markdown
is_shared 是否共享(服务端功能) 0/1
type_ 条目类型枚举 见下节,本条为 1(Note)

3. type_:条目的类型指纹

type_ 是序列化时最重要的判别字段,其枚举定义位于 BaseModel.ts

type_ 值 常量 含义 快照中示例
1 ModelType.Note 笔记 edd3cb39…(本文档)
2 ModelType.Folder 文件夹/笔记本 c4e45cad…folder1)、2fa39884…subFolder2
4 ModelType.Resource 附件资源 006a89df…photo.jpg
5 ModelType.Tag 标签 6cb91bb2…tag1
6 ModelType.NoteTag 笔记-标签关联 56748647…note_id + tag_id 字段)

不同 type_ 的条目文件携带各自的专属字段,例如标签条目(6cb91bb296ee458589eea0256ada06fa.md)没有正文与正文区,第一行 tag1 即标签名;NoteTag 关联条目(567486477f4249d38feadf6c5ec6e03d.md)则以 note_idtag_id 两个外键字段表达多对多关系。

三、快照从何而来:测试数据生成器

快照并非手工编写,而是由测试工具 syncTargetUtils.ts 在 CI 或本地自动生成。其核心逻辑(main())大致为:

  1. 初始化 Node 端 shim(sharpnodeSqlite)与测试数据库、同步器;
  2. 调用 createTestData(testData) 按预定结构写入数据;
  3. 若为 e2ee 类型,则启用加密并加载主密钥(setEncryptionEnabled(true)loadEncryptionMasterKey());
  4. 启动同步器执行完整同步;
  5. 将同步目录整体拷贝到 ${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 + tag2

recurseStructsyncTargetUtils.ts)递归遍历该树,parent_id 沿递归链传递——这解释了为何 note2parent_id 指向 subFolder22fa39884…),而 subFolder2parent_id 又指向 folder1c4e45cad…)。photo.jpg 来自 shim.attachFileToNote(note, ${supportDir}/photo.jpg)。测试代码注释中还给出了手动重建快照的命令:将 test-utilssyncTargetName_ 设为 filesystem 后运行 node tests/support/createSyncTargetSnapshot.js normal(及 e2ee),见 synchronizer_MigrationHandler.test.ts

与之对称的校验函数 checkTestDatasyncTargetUtils.ts)在同步/迁移完成后反向验证数据完整性:按标题加载文件夹与笔记、校验父文件夹、提取资源 URL 并加载资源、校验标签关联——任何一环缺失都会抛出错误。

四、快照的消费者:同步版本迁移测试

快照存在的根本目的是服务同步版本迁移测试synchronizer_MigrationHandler.test.tssynchronizer_MigrationHandler.test.ts)的测试思路是“取版本 n 的快照,升级到 n+1,验证数据未被改动”:

  1. deploySyncTargetSnapshot('normal', migrationVersion - 1) 将对应旧版快照拷贝为当前同步目录(syncTargetUtils.ts,先 fs.remove(syncDir)fs.copy(sourceDir, syncDir));
  2. 通过 fetchSyncInfo(fileApi()) 断言目标版本确实为 migrationVersion - 1
  3. Setting.setConstant('syncVersion', migrationVersion) 后调用 migrationHandler().upgrade(migrationVersion) 执行迁移;
  4. 再次 fetchSyncInfo 断言版本已升到 migrationVersion
  5. 校验目录结构断言(见 migrationTests,例如 v2/v3 均需存在 .resourcelockstemp 目录与 info.json 文件,且 .sync/version.txt 内容为 2);
  6. 到达最大版本后,运行完整同步并用 checkTestData(testData) 校验数据未被迁移改动;
  7. 最后 switchClient(2) 切换到第二个客户端再同步一次,模拟多客户端场景。

同样的流程对 e2ee 快照执行(synchronizer_MigrationHandler.test.ts):迁移后需用测试主密钥(密码 123456)加载密钥并启动 decryptionWorker 解密数据,再执行数据校验。

测试还预留了扩展点:新增迁移时需在 MigrationHandler.tsmigrations 数组追加迁移函数、在 Setting.syncVersion 提升版本号、并补充 migrationTests 断言(MigrationHandler.ts)。

五、版本标识的演进:info.json 与 .sync/version.txt

快照 v1、v2、v3 之间的差异正是同步版本机制的演进史,三者都体现在 migrations/ 目录下:

v1→v2(migration 2)

migrations/2.ts 一次性完成三项工作:

  • 写入 .sync/version.txt(内容 2)——注意:新格式下版本号已改存于根目录 info.json,但必须保留该旧文件,否则旧客户端会自动重建它并误判目标版本为 1;
  • 创建 locks/ 目录(Dirnames.Locks);
  • 创建 temp/ 目录(Dirnames.Temp)。

这解释了 v2 快照中同时存在 info.jsonlocks/ 的结构事实。

初版(migration 1)

migrations/1.ts 创建 .resource.sync.lock 三个隐藏目录并写入 .sync/version.txt = '1'——即最古老的版本标识方式,也是 fetchSyncTargetInfo 中“无 info.json 时回退读取 .sync/version.txt”逻辑的来源(MigrationHandler.ts)。

v2→v3(migration 3)

migrations/3.ts 不再改动目录结构,而是将本地缓存的 SyncInfosyncInfo.version = 3)通过 uploadSyncInfo 写入 info.json

版本探测与防错

fetchSyncInfosyncInfoUtils.ts)完整呈现了版本判定优先级:

  1. 存在 info.json → 解析 JSON,缺失 version 字段直接抛错;
  2. info.json 但有 .sync/version.txt → 视为 v1(需升级);
  3. 两者皆无 → 触发 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 当前活动主密钥 ID
masterKeys 主密钥列表(上传时通过 filterSyncInfo 剥离 content/checksum 等敏感属性)
noteLockKey 笔记锁密钥
ppk 端到端加密公私钥对(过滤时截断显示)
appMinVersion 可同步的最小应用版本(仓库中为 3.7.0 级常量,见 syncInfoUtils.ts
revisionServiceEnabled / revisionServiceTtlDays 修订历史服务开关与保留天数(默认 90 天)

其中 e2eeactiveMasterKeyId 等参数带 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 目录为同步迁移测试提供黄金样本,而 syncTargetUtilsMigrationHandlersyncInfoUtils 三个模块则分别负责快照的生成、迁移执行与版本探测。理解这份格式,不仅能读懂任意快照内容,也能在排查同步版本冲突、数据完整性校验与多客户端兼容问题时快速定位根因。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.16 K
2.78 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
904
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
932
1.86 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
862
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.95 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.38 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
535
606
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
549
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23