数据迁移 - lolly-backup

Lolly 用户积累的一切都保存在自己的设备上 - 没有账号,没有云端。数据迁移包就是这些数据移动的方式:在一个安装实例上导出它,通过任何方式携带该文件(USB、AirDrop、发送给自己的邮件、网络共享),再在另一个实例上导入。文件本身就是传输媒介。目标设备是否联网都无所谓,因为整个过程从不与任何服务器通信。

移动整个安装内容的两个按钮:导出我的数据写出一个 zip,导入数据将其读回signed by Lollyvector SVG自己验证Get the signed file12 paths~5.1k nodes12 groups60 KB移动整个安装内容的两个按钮:导出我的数据写出一个 zip,导入数据将其读回signed by Lollyvector SVG自己验证Get the signed file12 paths~5.1k nodes12 groups60 KB

本页是格式规范。终端用户的操作说明参见 使用 Lolly → 迁移到另一台设备。实现代码见 shells/web/src/data-transfer.ts,tests/data-transfer.test.ts 固定了往返转换的约定。

范围。 迁移包携带的是用户数据,而非工具。工具和目录资源是单独同步的,并假定目标设备上已经存在(最坏情况下版本更高)。导入操作绝不会安装或升级工具。

目标

外层封装

备份包是一个普通的 .zip。下载文件以所属者命名 - LollyTools-<First>-<Last>-<YYYY-MM-DD>-<n>.zip(例如 LollyTools-Ada-Lovelace-2026-06-26-1.zip)- 这样下载文件夹里的一堆备份仍然清晰可辨。名和姓来自个人资料,未设置时会省略。没有个人资料时得到 LollyTools-2026-06-26-1.zip,只有名字时得到 LollyTools-Ada-2026-06-26-1.zip。每一部分都会被清理为文件名安全的标记(保留 Unicode 字母/数字,去除空格/标点,上限 32 个字符)。<n> 是按设备、按天递增的序号,所以同一天多次导出不会冲突,并保持顺序。shells/web/src/data-transfer.ts 中的 backupFilename() 负责生成这个名称。压缩包内容与名称无关,始终一致。内容包括:

路径是否必需内容
manifest.json格式 id、版本、数量以及各部分的完整性校验。读取者最先查看的内容。
profile.json设置时用户的 me 记录(姓名、联系方式、头像引用、标志位)。通过 host.profile 读取。
sessions.json每个已保存的会话:插槽、工具 id/版本、标签、缩略图(data-URL)和完整的输入数据。通过 host.state 读取。
assets.json每个已上传资源(图片、字体、品牌令牌)的元数据,各自指向 assets/blobs/ 下的字节内容。
assets/blobs/<n>.<ext>按资源计原始资源字节(图片和字体文件)。未压缩存储(已经是压缩格式)。扩展名仅作外观标识,以 assets.json 中的 MIME 为准。
prefs.json用户自有的本地偏好设置:themesidebarWidth 以及 ct-metrics 活动统计。
lolly.txt迁移包的人类可读摘要(数量、个人资料、文件名),供未使用 Lolly 打开压缩包的人查看。每次导出都会重新生成,导入时可被识别,因此从不计入被跳过的部分。它是在完整性映射之后写入的,因此不在该映射范围内。

迁移包特意采用普通的压缩包格式:它能完好经受任何传输方式,任何解压工具都能查看它。

profile.json 是最小的一部分,也是应用中读取者最先看到的部分:制作者只需填写一次的详细信息,以及允许工具使用这些信息的选择开关。

成为 profile.json 的个人资料详情表单 - 姓名、联系方式、头像以及旁边的选择开关signed by Lollyvector SVG自己验证Get the signed file18 paths~2.0k nodes41 groups30 KB成为 profile.json 的个人资料详情表单 - 姓名、联系方式、头像以及旁边的选择开关signed by Lollyvector SVG自己验证Get the signed file18 paths~2.0k nodes41 groups30 KB

manifest.json

{
  "format": "lolly-backup",
  "formatVersion": 1,
  "minReader": 1,
  "app": "lolly",
  "exportedAt": "2026-06-22T09:30:00.000Z",
  "counts": { "profile": true, "sessions": 2, "userAssets": 4, "prefs": 3 },
  "integrity": {
    "profile.json": "sha256-…",
    "sessions.json": "sha256-…",
    "assets.json": "sha256-…",
    "assets/blobs/0.webp": "sha256-…",
    "prefs.json": "sha256-…"
  }
}
字段含义
format恒为 lolly-backup。没有该字段的文件会被拒绝,提示“不是 Lolly 备份”。
formatVersion该迁移包写入时所用的布局。对部分集合或结构的任何更改都会使其递增。读取者不会以此作为判断依据。
minReader安全导入该迁移包所需的最低读取器版本。读取者正是以此字段作为判断依据。
app生成该包的应用 id,用于诊断。
exportedAt迁移包创建时的 ISO 时间戳。
counts写入者放入的内容数量,用于显示和合理性检查。
integrity可选。将除 manifest.json 外的每个部分映射为其未压缩字节的 SRI 风格 sha256-<base64> 摘要。

版本策略(向前兼容)

formatVersionminReader 之间的区分,正是让该格式得以演进而不至于抛弃旧版安装的关键:

给作者的经验法则: 如果现有的每个读取者在忽略你新增内容的情况下仍能正确工作,那这项更改就是累加式的 - 递增 formatVersion,保持 minReader 不变。否则就提高 minReader

完整性

manifest.integrity 存在时,读取者会在写入任何内容之前验证列出的每个部分的 SHA-256。不匹配(“未通过完整性检查”)或缺失某个部分(“不完整”)都会中止整个导入 - 不存在部分恢复。这能捕获文件传输过程中可能引入的损坏(被截断的 AirDrop 传输、重新编码附件的邮件网关、损坏的 USB 扇区)。

完整性校验在设计上是尽力而为的:只有在 Web Crypto 可用时(每个安全浏览器上下文和现代 Node)才会写入,只有当映射和 Web Crypto 同时存在时才会验证。没有该映射的迁移包 - 例如完整性机制出现之前生成的包 - 会照常导入。“无法验证”永远不会被当作“已损坏”处理。

清单既不会列出自身,也不会列出重新生成的 lolly.txt 说明文件。摘要只覆盖清单所担保的那些部分。

导入语义

导入是合并覆盖,绝不会全部替换:

已保存的会话会自动重新关联其图片:资源引用以 id 保留,桥接层会在已上传图片恢复后重新解析它们(无论如何都必须如此,因为 blob: URL 无法在重新加载后存活)。

导入摘要会报告 { profile, sessions, userAssets, prefs, skipped, failedAssets }failedAssets 统计无法恢复的已上传资源数量(例如设备存储已满)。它与 skipped 不同,后者统计的是当前版本无法识别的、来自向前兼容的更新写入者的部分。界面会展示 skipped(“… · N 项较新内容已跳过”),因此恢复过程会如实说明遗漏了什么。

不会随迁移包传输的内容

存储用量表列出的正是同样的划分。已保存的会话和“我的图片”会包含在迁移包中。它们下方的资源缓存、工具预览和离线固定项都是可重新生成的,因此不会包含在内。

存储用量表将本设备的数据划分为若干命名类别,已保存的会话和我的图片与资源缓存分开统计,此处为全新安装、每个类别仍为空的状态signed by Lollyvector SVG自己验证Get the signed file25 paths~4.8k nodes46 groups59 KB存储用量表将本设备的数据划分为若干命名类别,已保存的会话和我的图片与资源缓存分开统计,此处为全新安装、每个类别仍为空的状态signed by Lollyvector SVG自己验证Get the signed file25 paths~4.8k nodes46 groups59 KB

跨 shell 保证

data-transfer.ts 只通过能力桥接层(host.profilehost.statehost.assets)以及共享的 localStorage 偏好设置进行读写。由于桥接层是唯一的接口,即使底层存储不同(web 上是 IndexedDB,Tauri 上是文件系统),同一个模块在每个 shell 上都会产生字节级一致的包。Tauri shell 复用这个模块,不做任何改动,只有它们的 host.state 实现不同。无头测试针对内存桥接层执行了完整的往返流程,这也是它能代表所有场景的原因。

有两个 shell 出于不同原因不在这个保证范围内:

预留扩展点

这个信封结构在设计上就是一份清单加上一组具名部分,这样新类型的可移植数据以后就能在不引入破坏性变更的情况下搭载进来。它们会以附加部分的形式插入(新的 formatVersion,相同的 minReader),而当前版本的读取器会跳过它不认识的内容。这些内容在路线图上,尚未实现。这里先预留这些名称,以便这些功能落地时格式仍保持一致。

除了这些预留名称和上面列出的部分之外的任何内容,对读取器来说都是未知部分:保持原样不动,并计入 skipped

参考